OVH Cloud OVH Cloud

Template et constante

64 réponses
Avatar
Etienne Rousee
Bonjour,

Le code suivant ne compile pas parce que
k n'est pas une constante:

template <int N> class Entier
{
........
};

int k = 5;

Entier <k> l;

Y aurait il une autre construction permettant de faire ça ?
Mon contexte est l'implémentation des anneaux Z/nZ,
des polynômes à coeefficients là dedans, et de quelques
calculs de groupes.

--

Etienne

10 réponses

3 4 5 6 7
Avatar
James Kanze
Vincent Lascaux wrote:
Le code ci-dessus pourrait marcher si la définition de
"EntierDynamique" le permettait.


Je crois que tu n'as pas compris le but de ma classe
EntierDynamique (qui a un très mauvais nom, ce qui explique
surement ton incompréhension en partie).

Le but n'est pas de faire un wrapper de la classe template
Entier, mais un wrapper du CODE qui utilise entier, de façon à
forcer le compilateur à compiler ce code pour chacun des N
possibles (de ce point de vue, c'est un peu comme la
compilation à la volée de James, mais en portable, et avec la
compilation faite pour tout les N à l'avance).


Je crois que ce n'est pas si important. Il faut voir les
alternatifs à ta disposition.

On ne crée par un vecteur de ma classe, on crée un fonctor
template qui sera utilisé par ma classe pour chaque valeur de
N. Au lieu d'écrire

std::cin >> N;
switch (N)
{
case 1: Entier<1> x(34); std::cout << x << std::endl; break;
case 2: Entier<2> x(34); std::cout << x << std::endl; break;
case 3: Entier<3> x(34); std::cout << x << std::endl; break;
case 4: Entier<4> x(34); std::cout << x << std::endl; break;
case 5: Entier<5> x(34); std::cout << x << std::endl; break;
.....
case 10000: Entier<10000> x(34); std::cout << x << std::endl; break;
}

on écrit

struct output34
{
template<int N>
void Execute()
{
Entier<N> x(34);
std::cout << x << std::endl;
}
};

std::cin >> N;
EntierDynamic<0, 10000>::Execute(N, output34());

On conserve ainsi le typage fort, qui a ses interet pour permettre au
compilo de faire des optimisations (c'est probablement irrélevant pour le
cas ici, mais ca pourrait l'être dans d'autres, comme pour le filtre
gaussien que j'ai évoqué dans un autre message de ce thread).


En dehors des optimisations, la solution avec un paramètre
référence (disons int const& N) conserve le typage fort.

Tes options sont limitées. L'instantiation d'un template se fait
lors de la compilation (ou au plus tard, lors d'une édition de
liens statique), et exige une constante connue du compilateur.
Alors, tu as comme options :

-- La compilation dynamique, comme j'ai expliqué. Tu lis N, et
puis tu lances le compilateur pour générer un objet
dynamique, que tu charges par la suite.

En fait, si j'ai bien compris le problème, ça fait
exactement ce que tu veux. Seulement, c'est vraiment lourd,
et il exige que l'utilisateur du code ait un compilateur
installé chez lui -- le même compilateur, dans la même
version, qui t'a servi à compiler ton programme.

-- Si tu restreins le nombre de N que tu supportes, tu pourrais
les générer les objets dynamiques tous en avance, avec une
convention de nommage, et charger celui que tu veux une fois
que tu aurais lu N. Seulement, si tu veux supporter tous les
N de 2 à INT_MAX, ça te fait quelque 2 milliards d'objets
dynamiques (.so ou .dll, selon le système). Chacun compilé
individuellement, donc, un build avec 2 milliards de
compilation, et deux milliards d'éditions de liens. Je ne
crois pas que ça tient le coup -- tu peux calculer l'espace
disque qu'il te faut toi-même, et pour le temps de build, il
y a à peu près 3 milliards de sécondes dans un siècle.

C'est une solution envisageable seulement dans la mesure où
tu pourrais limiter le choix de N d'une dizaine de valeurs
pré-définies, tout au plus.

-- Il y a le template instantié sur une référence. Tout est
donc reglé lors de la compilation. Tu as les validations de
type, mais tu n'as pas des optimisations éventuelles du fait
que le compilateur sache la valeur, puisqu'il ne le sait
pas. Tu as toujours autant de copies du code généré que tu
as d'instantiations du template, même s'il sont toutes
identiques.

-- Tu utilise une classe non templatée, avec N comme paramètre
du constructeur. Tu n'as qu'une copie du code, quoiqu'il
arrive. Mais toutes les instances, même avec des N
différentes, sont du même type -- une vérification du
melange des « types » ne peut se faire qu'à l'exécution.
Et il n'y a pas plus de possibilités d'optimisation qu'avec
la solution précédante.

Ce sont a priori les seules solutions que je vois. C'est à toi
de choisir.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Vincent Lascaux
En dehors des optimisations, la solution avec un paramètre
référence (disons int const& N) conserve le typage fort.


Non...
const int x = 1;
const int y = 1;
Entier<x> et Entier<y> seraient des types différents.

Si on veut faire une classe polynome avec le degré en parametre template, si
on utilise un reférence, on n'a pas possibilité (enfin, il me semble)
d'écrire la dérivation (qui retourne un polynome de degré moindre).

-- Il y a le template instantié sur une référence. Tout est
donc reglé lors de la compilation. Tu as les validations de
type, mais tu n'as pas des optimisations éventuelles du fait
que le compilateur sache la valeur, puisqu'il ne le sait
pas. Tu as toujours autant de copies du code généré que tu
as d'instantiations du template, même s'il sont toutes
identiques.


En fait j'ai du mal à saisir l'interet de cette méthode. Par rapport à celle
qui consiste à passer N au constructeur, pourquoi est-ce plus typé ? Pour le
problème cité ici, tu utiliserais combien d'entier statics en tout ? Si tu
n'en utilise qu'un c'est identique à utiliser une variable globale pour
stoquer N...

--
Vincent

Avatar
James Kanze
wrote:
James Kanze wrote:
wrote:

Oui, l'opérateur+ est en général un opérateur non-membre ami. Outre
ce que tu as dis, il y a une autre raison à utiliser ce design. Dans
la version non-membre, les 2 opérandes peuvent être le résultat
d'une conversion automatique. Dans la version membre, l'opérande de
gauche (paramète implicite this) ne peut pas être le résultat d 'une
telle conversion.


Oui et non. Très, très souvent, l'implémentation de l'operator+
est simplement :

MonType
operator+( MonType const& lhs, MonType const& rhs )
{
MonType tmp( lhs ) ;
lhs += rhs ;
return lhs ;
}

Et évidemment, le constructeur de copie et l'opérateur += sont
des fonctions publiques. Du coup, aucune raison de faire
operator+ un ami.

Sauf, évidemment, si on veut généraliser cette technique, au
moyen du Barton-Nackman trick. Mais alors, les opérateurs ne sont
pas amis afin d'accéder aux éléments privés, mais seulement
parce que c'est la seule façon de fournir l'implémentation d'une
fonction libre dans la définition de la classe. (Voir le
composant Operators, sous-système Basic dans ma bibliothèque
utilitaire, par exemple.)


Exact, en supposant que tu voulais écrire :
MonType tmp( lhs ) ;
lhs += rhs ;
return lhs ;


MonType tmp( lhs ) ;
tmp += rhs ;
return tmp ;


En effet.

Cependant, dans certains domaines (surtout celui de ce fil de
discussion, c'est-à-dire celui de classes représentant des valeurs
numériques), on peut utiliser une définition d'operator+ (ami) de ce
genre :

MonType
operator+( MonType const& lhs, MonType const& rhs )
{
return MonType(/*opérations sur les membres de lhs et rhs*/);
}


C'est une optimisation qui pourrait être utile dans certains
cas, oui. Il y a aussi des cas où c'est plus logique de définir
+= en termes de +, puis =; c'est souvent le cas des classes de
string, par exemple. (Encore que dans le cas de string, je
définis les deux en termes d'une fonction membre « replace ».)
Mais je crois que ce sont plutôt des exceptions (et je me
méfierais d'une classe avec des implémentations indépendantes de
+ et de += -- l'un doit dépendre de l'autre, afin d'être
certain qu'elles ont une sémantique compatible).

Cela permet au compilateur d'effectuer des optimisations
supplémentaires. Sinon, dans la version avec l'opérateur +=, il n'e st
pas certain que le compilateur supporte la " named return value
optimization".


CFront le supportait. Il y a plus de quinze ans. Il n'y a pas
d'excuse qu'un compilateur moderne ne le supporte pas.

On pourrait aussi écrire :

MonType
operator+( MonType const& lhs, MonType const& rhs )
{
return MonType ( lhs ) += rhs;
}

Mais très peu de compilateurs pourront utiliser la RVO ici étant
donné la complexité de l'instruction. D'ailleurs, la manière
d'optimiser dans ces cas-là n'est pas normalisée.


La manière d'optimiser n'est jamais normalisée. On pourrait
imaginer un compilateur qui reconnaît certains modèles, et les
traite spécialement, avec une optimisation accrue. Je ne vois
pas de raison de supposer a priori que l'une des trois solutions
proposées soit systèmatiquement plus rapide ; si, comme CFront,
le compilateur implémente NRVO mais non RVO en général, il y a
de fortes chances que si la copie est chère, ma première
solution en soit la plus rapide. Mais sans faire des mesures, on
n'en sait rien, et avec des mesures, on n'en sait quelque chose
que pour une version donnée d'un compilateur donné sur une
plateforme donnée.

J'utilise systèmatiquement la première version que j'ai
présentée, parce qu'il me paraît la plus propre, et la plus
facile à implémenter. (En fait, j'utilise le Barton and Nackman
trick -- la classe MaClasse hérite de
ArithmeticOperators< MaClasse >, et si elle implémente +=,
l'opérator + y est, sans que j'ai quoique ce soit à faire.)

Enfin, on pourrait également écrire :

MonType
operator+( MonType lhs, MonType const& rhs )
{
return lhs += rhs;
}

Mais ce sera de toute façon moins performant qu'une version avec RVO.


Selon la version, le compilateur et la plateforme. On ne peut
pas dire des choses comme ça d'avance.

En tout les cas, et si c'est possible, il vaut mieux utiliser la
version operator+ amie qui sera optimisable par un plus grand nombre de
compilateurs.


N'importe quoi. D'abord, tu n'en sais rien, et puis, la plupart
du temps, toutes les versions seront suffisamment rapides, et il
serait carrément sot de ne pas adopter la version la plus facile
à comprendre et à maintenir.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
James Kanze
Sylvain wrote:
wrote on 02/11/2006 19:04:

Exact, en supposant que tu voulais écrire :

MonType tmp( lhs ) ;
tmp += rhs ;
return tmp ;


qui aura ""l'inconvient" de nécessiter un temporaire, même pour des
structures simplissimes (comme celle de l'exemple).


Il n'exige pas plus d'instances de la classe que n'importe
laquelle des autres versions.

On pourrait aussi écrire :

MonType
operator+( MonType const& lhs, MonType const& rhs )
{
return MonType ( lhs ) += rhs;
}


même "défaut".

Enfin, on pourrait également écrire :

MonType
operator+( MonType lhs, MonType const& rhs )
{
return lhs += rhs;
}


non ça on ne pourra pas, lhs est modifié - ce qui n'est pas souhait é.


Attention : il a modifié la signature pour la passer lhs par
copie, non par référence.

Un essaie très rapide avec mon compilateur (g++ 4.1.0, Solaris,
Sun Sparc) indique que j'ai le même nombres d'objets avec :

T tmp( lhs ) ;
tmp += rhs ;
return tmp ;

et une version amie, et un objet de plus avec les deux autres
versions ci-dessus. Même chose avec VC++ 8.0, d'ailleurs.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
James Kanze
Sylvain wrote:
wrote on 02/11/2006 23:12:

toutefois, je ne crois pas à la création "brute d'un temporaire", je
veux dire si la structure est compliquée, on aura fourni un constructeur
de copie aussi intelligent que possible.


Si la structure est compliquée, et on supporte les opérateurs
arithmétique (et donc, des expressions compliquées), et qu'on
s'intéresse à la performance, on utilise les expressions
templatées ou d'autres techniques (autrefois, avec des fonctions
virtuelles) pour éviter des copies. Donc, par exemple,
l'operator+ ne renvoie pas une instance de l'objet, mais une
vertice qui sait calculer des valeurs individuelles à partir de
ces paramètres. Et on n'évalue l'expression qu'au besoin, sans
jamais créer les intermédiaires.

Jette un coup d'oeil dans n'importe quelle bibliothèque
matricielle, par exemple.
(http://www.oonumerics.org/blitz/index.html est probablement le
plus connu, peut-être parce que c'était une des premières a
utilisé des templates, plutôt qu'une hièrarchie des classes avec
des fonctions virtuelles, pour les vertices dans la graphe de
l'expression.)

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
fabien.chene
"James Kanze" writes:

Fabien Chêne wrote:
"James Kanze" writes:

A noter que:

template <int& N>
class foo { ... };

int x;

foo<x> bar;

est possible.


Est-ce que « template< int const& N > » ne serait pas mieux ?
Voire même avec « int const » comme paramètre. (Mais
attention : il faut que l'int ait un linkage global, donc,
qu'il soit défini dans une portée référentielle et qu'il ait une
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

durée de vie statique.


Est-ce que cela exclu la définition dans une autre TU ?


Non, pourquoi est-ce qu'il l'exclurait ? (En fait, toute
variable définie dans une portée référentielle a une durée de
vie statique. L'inverse n'est pas forcément vrai.)


J'avais du mal avec le terme «portée référentielle» ; si c'est la
traduction de «namespace scope» alors c'est bon pour moi :-)


--
Fab




Avatar
Sylvain
Vincent Lascaux wrote on 03/11/2006 07:25:
[...]
pour n'utiliser que des entiers modulo 8, il sera content que le compilateur
fasse un shift de bits plutot qu'un modulo...).


shifter ?!
tu veux surement dire un &= 7

Sylvain.

Avatar
fabien.chene
"James Kanze" writes:

[ ... ]
J'utilise systèmatiquement la première version que j'ai
présentée, parce qu'il me paraît la plus propre, et la plus
facile à implémenter. (En fait, j'utilise le Barton and Nackman
trick -- la classe MaClasse hérite de
ArithmeticOperators< MaClasse >, et si elle implémente +=,
l'opérator + y est, sans que j'ai quoique ce soit à faire.)


Boost.Operators propose quelquechose d'identique. D'ailleurs en
regardant leur implémentation, on constate qu'ils utilisent la
première version que tu as présenté lorsque le compilateur implémente
l'optimisation NRVO. Dans le cas contraire, ils utilisent la troisième
version :

T operator+( T lhs, T const& rhs )
{ return lhs += rhs; }

D'ailleurs, il y a une macro qui tendrait à dire que cette troisième
version n'est pas symétrique. Je ne vois pas pourquoi, peut-être
est-ce au bug de compilateur près ?

Sinon, comme suggéré par Herb, retourner «T const» au lieu de «T»
empêche d'écrire a + b = c; Je ne sais pour quelle raison boost ne
l'utilise pas.


--
Fab

Avatar
Gabriel Dos Reis
(Fabien Chêne) writes:

| "James Kanze" writes:
|
| > Fabien Chêne wrote:
| >> "James Kanze" writes:
| >
| >> >> A noter que:
| >
| >> >> template <int& N>
| >> >> class foo { ... };
| >
| >> >> int x;
| >
| >> >> foo<x> bar;
| >
| >> >> est possible.
| >
| >> > Est-ce que « template< int const& N > » ne serait pas mieux ?
| >> > Voire même avec « int const » comme paramètre. (Mais
| >> > attention : il faut que l'int ait un linkage global, donc,
| >> > qu'il soit défini dans une portée référentielle et qu'il ait une
| >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| >> > durée de vie statique.
| >
| >> Est-ce que cela exclu la définition dans une autre TU ?
| >
| > Non, pourquoi est-ce qu'il l'exclurait ? (En fait, toute
| > variable définie dans une portée référentielle a une durée de
| > vie statique. L'inverse n'est pas forcément vrai.)
|
| J'avais du mal avec le terme «portée référentielle» ; si c'est la
| traduction de «namespace scope» alors c'est bon pour moi :-)

namespace -> espace référentiel
scope -> portée
namespace scope -> portée d'un espace référentiel


Il y a quelques distinctions à faire (aussi subtile en standardais
qu'en gaulois) : tout espace référentiel est introduit par une
déclaration, donc som *nom a une portée* (comme toute déclaration).
Un espace référentiel est une suite de déclarations, donc *possède une
portée*. Les gens ont tendance à identifier une déclaration avec son
nom. Tu pourrais donc entendre quelque chose comme « cet espace
référentiel a une portée d'espace référentiel. »

-- Gaby
Avatar
Gabriel Dos Reis
"James Kanze" writes:

[...]

| cas, oui. Il y a aussi des cas où c'est plus logique de définir
| += en termes de +, puis =; c'est souvent le cas des classes de
| string, par exemple. (Encore que dans le cas de string, je
| définis les deux en termes d'une fonction membre « replace ».)
| Mais je crois que ce sont plutôt des exceptions (et je me
| méfierais d'une classe avec des implémentations indépendantes de
| + et de += -- l'un doit dépendre de l'autre, afin d'être
| certain qu'elles ont une sémantique compatible).

La classe d'exception dépend dans quel coin du monde tu regardes. :-)

-- Gaby
3 4 5 6 7