La programmation générique consiste à programmer avec des types et des algorithmes paramétrés par des types, des valeurs et des algorithmes.
En C++, la programmation générique est prise en charge par les mécanismes du langage template.
Les arguments des fonctions génériques se caractérisent par des ensembles d’exigences sur les types et les valeurs des arguments impliqués.
En C++, ces exigences sont exprimées par des prédicats temps de compilation appelés concepts.
Les modèles peuvent également être utilisés pour le méta‑programmation ; c’est‑à‑dire, des programmes qui composent du code à la compilation.
Une notion centrale de la programmation générique est le « concept » ; c’est‑à‑dire, des exigences sur les arguments de modèle présentées comme des prédicats temps de compilation.
Les concepts ont été standardisés en C++20, bien qu’ils aient été accessibles pour la première fois, sous une syntaxe légèrement plus ancienne, dans GCC 6.1.
Résumé des règles d’utilisation des modèles :
Résumé des règles d’utilisation des concepts :
auto pour les variables localesRésumé des règles de définition de concepts :
!C<T>) avec parcimonie pour exprimer une différence mineure @TODO-LINKC1<T> || C2<T>) avec parcimonie pour exprimer des alternatives @TODO-LINKRésumé des règles d’interface de modèle :
using à typedef pour définir des aliasenable_ifRésumé des règles de définition de modèle :
{} plutôt que () dans les modèles pour éviter les ambiguïtésRésumé des règles de modèles et hiérarchies :
Résumé des règles de modèles variadiques :
Résumé des règles de méta‑programmation :
constexpr pour calculer des valeurs à la compilationRésumé des autres règles de modèle :
static_assertLa programmation générique consiste à programmer avec des types et des algorithmes paramétrés par des types, valeurs et algorithmes.
Généralité. Réutilisation. Efficacité. Encourage une définition cohérente des types utilisateur.
Conceptuellement, les exigences suivantes sont incorrectes car ce que nous attendons de T est plus que les concepts très basiques de « peut être incrémenté » ou « peut être ajouté »:
template<typename T>
requires Incrementable<T>
T sum1(vector<T>& v, T s)
{
for (auto x : v) s += x;
return s;
}
template<typename T>
requires Simple_number<T>
T sum2(vector<T>& v, T s)
{
for (auto x : v) s = s + x;
return s;
}
En supposant que Incrementable ne supporte pas + et que Simple_number ne supporte pas +=, nous avons surconstrained les implémentateurs de sum1 et sum2. Et, dans ce cas, une opportunité de généralisation a été perdue.
template<typename T>
requires Arithmetic<T>
T sum(vector<T>& v, T s)
{
for (auto x : v) s += x;
return s;
}
En supposant que Arithmetic exige à la fois + et +=, nous avons contraint l’utilisateur de sum à fournir un type arithmétique complet. Ce n’est pas une exigence minimale, mais elle donne à l’implémenteur d’algorithmes la liberté beaucoup nécessaire et assure que tout type Arithmetic peut être utilisé pour une vaste variété d’algorithmes.
Pour une généralité et réutilisation supplémentaires, nous pourrions également utiliser un concept Container ou Range plus général au lieu de se limiter à un seul conteneur, vector.
Si nous définissions un modèle qui exige exactement les opérations requises pour une implémentation unique d’un seul algorithme (par exemple, ne requérant que += plutôt que aussi = et +) et uniquement celles‑ci, nous surconstringsions les mainteneurs. Nous visons à minimiser les exigences sur les arguments de modèle, mais les exigences absolument minimales d’une implémentation sont rarement un concept significatif.
Les modèles peuvent être utilisés pour exprimer essentiellement tout (ils sont Turing complets), mais le but de la programmation générique (comme exprimé via les modèles) est d’étendre efficacement les opérations/algorithmes sur un ensemble de types ayant des propriétés sémantiques similaires.
Généralité. Minimiser la quantité de code source. Interopérabilité. Réutilisation.
C’est la base du STL. Un seul algorithme find fonctionne facilement avec n’importe quel type de plage d’entrée :
template<typename Iter, typename Val>
// requires Input_iterator<Iter>
// && Equality_comparable<Value_type<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{
// ...
}
N’utilisez pas un modèle à moins d’avoir un besoin réaliste de plus d’un type d’argument de modèle. Ne surabstrait pas.
??? difficile, probablement nécessite un humain
Les conteneurs ont besoin d’un type d’élément, et l’exprimer comme un argument de modèle est générique, réutilisable et sûr. Cela évite également des contournements fragiles ou inefficaces. Convention : c’est ainsi que le STL le fait.
template<typename T>
// requires Regular<T>
class Vector {
// ...
T* elem; // pointe vers sz Ts
int sz;
};
Vector<double> v(10);
v[7] = 9.9;
class Container {
// ...
void* elem; // pointe vers size éléments de type inconnu
int sz;
};
Container c(10, sizeof(double));
((double*) c.elem)[7] = 9.9;
Cela n’exprime pas directement l’intention du programmeur et masque la structure du programme du système de type et de l’optimiseur.
Masquer l’void* derrière des macros n’est qu’une obscurité et introduit de nouvelles opportunités de confusion.
Exceptions : Si vous avez besoin d’une interface stable à l’ABI, vous pourriez être contraint à fournir une implémentation de base et exprimer le modèle (type‑safe) en fonction de celle‑ci. Voir Stable base.
void* et de conversions hors du code d’implémentation de bas niveau???
???
Exceptions : ???
Les techniques génériques et orientées objet sont complémentaires.
La polymorphie statique soutient la dynamique : utilisez la polymorphie statique pour implémenter des interfaces polymorphes dynamiques.
class Command {
// fonctions virtuelles pures
};
// implémentations
template</*...*/>
class ConcreteCommand : public Command {
// implémenter virtuelles
};
La dynamique soutient la statique : proposez une interface générique, confortable, liée statiquement, mais dont le dispatch interne est dynamique, afin d’offrir un agencement d’objet uniforme. Les exemples incluent l’abstraction de type comme le destructeur de std::shared_ptr (mais ne pas sureférence trop l’abstraction de type).
#include <memory>
class Object {
public:
template<typename T>
Object(T&& obj)
: concept_(std::make_shared<ConcreteCommand<T>>(std::forward<T>(obj))) {}
int get_id() const { return concept_->get_id(); }
private:
struct Command {
virtual ~Command() {}
virtual int get_id() const = 0;
};
template<typename T>
struct ConcreteCommand final : Command {
ConcreteCommand(T&& obj) noexcept : object_(std::forward<T>(obj)) {}
int get_id() const final { return object_.get_id(); }
private:
T object_;
};
std::shared_ptr<Command> concept_;
};
class Bar {
public:
int get_id() const { return 1; }
};
struct Foo {
public:
int get_id() const { return 2; }
};
Object o(Bar{});
Object o2(Foo{});
Dans un modèle de classe, les fonctions non virtuelles ne sont instanciées que lorsqu’elles sont utilisées – mais les fonctions virtuelles sont instanciées à chaque fois. Cela peut gonfler la taille du code et peut surconstrained un type générique en instanciant des fonctionnalités jamais nécessaires. Évitez‑ce, même si les facettes de la bibliothèque standard ont commis cette erreur.
Voir la référence aux règles plus spécifiques.
Les concepts sont une fonctionnalité C++20 pour spécifier les exigences pour les arguments de modèles. Ils sont cruciaux dans la réflexion sur la programmation générique et la base de nombreux travaux sur les futures bibliothèques C++ (standard et autres).
Cette section suppose le support des concepts.
auto pour les variables localesCorrection et lisibilité. Le sens supposé (syntaxe et sémantique) d’un argument de modèle est fondamental pour l’interface d’un modèle. Un concept améliore considérablement la documentation et le traitement des erreurs pour le modèle. Spécifier des concepts pour les arguments de modèle est un puissant outil de conception.
template<typename Iter, typename Val>
requires input_iterator<Iter>
&& equality_comparable_with<iter_value_t<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{
// ...
}
ou de façon équivalente et plus concise :
template<input_iterator Iter, typename Val>
requires equality_comparable_with<iter_value_t<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{
// ...
}
typename (ou auto) est le concept le moins contraignant. Il ne doit être utilisé que rarement lorsqu’aucune hypothèse plus précise n’est faite. C’est généralement uniquement nécessaire lorsque (dans le cadre du méta‑programmation) nous manipulons des arbres d’expression purs, en reportant le contrôle de type.
Références : TC++PL4
Marquer les arguments de type de modèle sans concept
Les concepts « standards » (fourni par le GSL et par le standard ISO lui‑même) nous font gagner le travail de créer nos propres concepts, sont mieux pensés que ceux que nous pourrions improviser à la hâte et améliorent l’interopérabilité.
À moins de créer une nouvelle bibliothèque générique, la plupart des concepts dont vous avez besoin seront déjà définis par la bibliothèque standard.
template<typename T>
// ne pas définir ceci : sortable est dans <iterator>
concept Ordered_container = Sequence<T> && Random_access<Iterator<T>> && Ordered<Value_type<T>>;
void sort(Ordered_container auto& s);
Ce Ordered_container est assez plausible, mais il est très proche du concept sortable dans la bibliothèque standard. Est‑il meilleur ? Est‑il juste ? Reflette‑il réellement les exigences du standard pour sort ? Il vaut mieux utiliser simplement sortable :
void sort(sortable auto& s); // mieux
La liste des concepts « standard » évolue à mesure que nous approchons d’un standard ISO incluant les concepts.
Concevoir un concept utile est difficile.
Réunir ce qui est faisable.
auto pour les variables localesauto est le concept le plus faible. Les noms de concepts portent plus de signification que simplement auto.
vector<string> v{ "abc", "xyz" };
auto& x = v.front(); // mauvais
String auto& s = v.front(); // bon (String est un concept GSL)
Lisibilité. Exprime directement une idée.
Pour dire « T est sortable » :
template<typename T> // Correct mais verbeux : "The parameter is
requires sortable<T> // of type T which is the name of a type
void sort(T&); // that is sortable"
template<sortable T> // Better : "The parameter is of type T
void sort(T&); // which is Sortable"
void sort(sortable auto&); // Best: "The parameter is Sortable"
Les versions plus courtes correspondent mieux à notre façon de parler. Notez que de nombreux modèles n’ont pas besoin d’utiliser le mot-clé template.
<typename T> et <class T> en version restreinte.typename puis le restreindre avec un concept simple à argument unique.Définir de bons concepts est non trivial. Les concepts visent à représenter des concepts fondamentaux d’un domaine d’application (c’est‑à‑dire le nom « concept »). Assembler un ensemble de contraintes syntaxiques pour être utilisés pour les arguments d’une seule classe ou d’un seul algorithme n’est pas ce que les concepts étaient conçus pour et ne donnera pas les avantages complets du mécanisme.
Il est généralement utile de définir des concepts pour le codage qui fonctionne avec une implémentation (p. ex., C++20 ou plus tard), mais définir des concepts est une technique de conception utile en soi et aide à attraper les erreurs conceptuelles et à clarifier les concepts (sic!) d’une implémentation.
Les concepts sont censés exprimer des notions sémantiques, comme « un nombre », « un intervalle d’éléments » et « totalement ordered ». Des contraintes simples, comme « a un opérateur + » ou « a un opérateur > », ne peuvent être significativement spécifiées en isolation et ne devraient être utilisées que comme blocs de construction pour des concepts signifiants, plutôt que dans le code utilisateur.
template<typename T>
// mauvais ; insuffisant
concept Addable = requires(T a, T b) { a + b; };
template<Addable N>
auto algo(const N& a, const N& b) // utilisez deux chiffres
{
// ...
return a + b;
}
int x = 7;
int y = 9;
auto z = algo(x, y); // z = 16
string xx = "7";
string yy = "9";
auto zz = algo(xx, yy); // zz = "79"
Peut-être qu’on attendait la concaténation. Plus probablement, c’était un accident. Définir l’opérateur de soustraction également yieldrait des ensembles d’acceptation très différents. Le Addable viole la règle mathématique d’additivité : a+b == b+a.
La capacité à spécifier une sémantique significative est une caractéristique décisive d’un véritable concept, contrairement à une contrainte syntaxique.
template<typename T>
// Les opérateurs +, -, *, et / sur un nombre sont supposés suivre les règles mathématiques usuelles
concept Number = requires(T a, T b) { a + b; a - b; a * b; a / b; };
template<Number N>
auto algo(const N& a, const N& b)
{
// ...
return a + b;
}
int x = 7;
int y = 9;
auto z = algo(x, y); // z = 16
string xx = "7";
string yy = "9";
auto zz = algo(xx, yy); // erreur : string n'est pas un Number
Les concepts avec plusieurs opérations ont une probabilité beaucoup plus faible de correspondre accidentellement à un type qu’un concept à une seule opération.
enable_if qui semble simuler des concepts à une opération unique.Facilité de compréhension. Interopérabilité améliorée. Aide les implémenteurs et les mainteneurs.
C’est une variante spécifique de la règle plus générale qu’un concept doit avoir une signification sémantique (voir [T.20]).
template<typename T> concept Subtractable = requires(T a, T b) { a - b; };
Cette définition n’a pas de sens sémantique. Vous avez besoin d’au moins + pour donner un sens à -.
Exemples d’ensembles complets :
Arithmetic : +, -, *, /, +=, -=, *=, /=Comparable : <, >, <=, >=, ==, !=Cette règle s’applique, que vous utilisiez le support natif des concepts ou non. C’est une règle générale qui s’applique même aux non‑templates :
class Minimal {
// ...
};
bool operator==(const Minimal&, const Minimal&);
bool operator<(const Minimal&, const Minimal&);
Minimal operator+(const Minimal&, const Minimal&);
// pas d’autres opérateurs
Un concept utile a un sens sémantique. Exprimer ce sens de façon informelle, semi‑formelle ou formelle rend le concept compréhensible et la démarche d’expression du concept peut attraper des erreurs conceptuelles. C’est un outil de conception puissant.
template<typename T>
// Les opérateurs +, -, *, et / sur un nombre sont supposés suivre les règles mathématiques usuelles
// axiom(T a, T b) { a + b == b + a; a - a == 0; a * (b + c) == a * b + a * c; /*...*/ }
concept Number = requires(T a, T b) {
{ a + b } -> convertible_to<T>;
{ a - b } -> convertible_to<T>;
{ a * b } -> convertible_to<T>;
{ a / b } -> convertible_to<T>;
};
C’est un axiome au sens mathématique : quelque chose qui peut être supposé sans preuve. En général, les axiomes ne sont pas démontrables, et lorsque cela se produit, la preuve dépasse souvent les possibilités d’un compilateur.
Un axiome peut ne pas être général, mais l’écrivain du modèle peut supposer qu’il est valable pour toutes les entrées réellement utilisées (comme une précondition).
Dans ce contexte les axiomes sont des expressions booléennes.
Voir le Palo Alto TR pour des exemples.
Actuellement, C++ ne prend pas en charge les axiomes (même la TS Concepts ISO), alors on se doit de rester en commentaires longtemps.
Une fois le support linguistique disponible, le // devant l’axiome peut être enlevé.
Les concepts de « ébauche » encore en développement ne définissent généralement qu’un ensemble de contraintes sans sémantique bien définie.
Un « Balancer » pour un arbre binaire générique :
// balancer pour un generic binary tree
template<typename Node> concept Balancer = requires(Node* p) {
add_fixup(p);
touch(p);
detach(p);
};
Donc un Balancer doit fournir au minimum ces opérations sur un nœud Node, mais nous ne sommes pas encore prêts à spécifier la sémantique générale complète car un nouvel arbre équilibré peut nécessiter plus d’opérations.
Un concept incomplet ou sans sémantique bien définie reste utile.
Sinon ils ne peuvent être distingués automatiquement par le compilateur.
template<typename I>
// Note : input_iterator est défini dans <iterator>
concept Input_iter = requires(I iter) { ++iter; };
template<typename I>
// Note : forward_iterator est défini dans <iterator>
concept Fwd_iter = Input_iter<I> && requires(I iter) { iter++; };
Le compilateur peut déterminer l’affinement basé sur l’ensemble des opérations requises (ici, suffix ++). Cela réduit la charge sur les implémenteurs de ces types car ils n’ont pas besoin de déclarations spéciales pour « hooker » le concept.
Si deux concepts ont exactement les mêmes exigences, ils sont logiquement équivalents (il n’y a pas d’affinement).
Deux concepts qui exigent la même syntaxe mais ont des sémantiques différentes créent une ambiguïté à moins que le programmeur ne les différencie.
template<typename I> // iterator fournissant un accès aléatoire
// Note : random_access_iterator est défini dans <iterator>
concept RA_iter = ...;
template<typename I> // iterator fournissant un accès aléatoire à des données contiguës
// Note : contiguous_iterator est défini dans <iterator>
concept Contiguous_iter =
RA_iter<I> && is_contiguous_v<I>; // utilisant le trait is_contiguous
Le programmeur (dans une bibliothèque) doit définir is_contiguous (un trait) correctement.
Envelopper une classe‑étiquette dans un concept simplifie encore ce motif :
template<typename I> concept Contiguous = is_contiguous_v<I>;
template<typename I>
concept Contiguous_iter = RA_iter<I> && Contiguous<I>;
Le programmeur doit définir le trait is_contiguous approprié.
Les traits peuvent être des classes‑traits ou des traits de type.
Ils peuvent être définis par l’utilisateur ou par la bibliothèque standard.
Préférer ceux de la bibliothèque standard.
Clarté. Maintenabilité. Les fonctions avec des exigences complémentaires exprimées via la négation sont fragiles.
Initie‑vous à un concept :
template<typename T>
requires !C<T> // mauvais
void f();
ou
template<typename T>
requires C<T>
void f();
C’est meilleur :
template<typename T> // modèle général
void f();
template<typename T> // spécialisation par concept
requires C<T>
void f();
Le compilateur choisira la version non contraint seulement lorsque C<T> n’est pas satisfait.
Si vous ne voulez pas (ou ne pouvez pas) définir une version non contraint de f, supprimez‑la.
template<typename T>
void f() = delete;
Le compilateur choisira l’overload, ou émettra une erreur appropriée.
Les exigences complémentaires sont couramment trouvées dans enable_if :
template<typename T>
enable_if<!C<T>, void> // mauvais
f();
template<typename T>
enable_if<C<T>, void>
f();
Les exigences complémentaires sur une seule exigence sont parfois (incorrectement) considérées gérables.
Mais pour deux ou plus exigences, le nombre de définitions peut multiplier exponentiellement (2, 4, 8, 16, …):
C1<T> && C2<T>
!C1<T> && C2<T>
C1<T> && !C2<T>
!C1<T> && !C2<T>
Les opportunités d’erreurs se multiplient.
C<T> et !C<T>.La définition est plus lisible et correspond directement à ce que l’utilisateur doit écrire.
Les conversions sont prises en compte. Vous n’avez pas à vous souvenir des noms de tous les traits de type.
Vous pourriez être tenté de définir un concept Equality ainsi :
template<typename T> concept Equality = has_equal<T> && has_not_equal<T>;
Il est clairement meilleur et plus facile d’utiliser le standard equality_comparable, mais - à titre d’exemple - si vous deviez définir un tel concept, préférez :
template<typename T> concept Equality = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
// axiom { !(a == b) == (a != b) }
// axiom { a = b; => a == b } // => signifie "implique"
};
au lieu de définir deux concepts sans signification has_equal et has_not_equal juste comme auxiliaires.
????
Tout au long des années, la programmation avec des modèles a souffert d’une faible distinction entre l’interface d’un modèle et son implémentation.
Avant les concepts, cette distinction n’était pas directement prise en charge par le langage.
Cependant, l’interface d’un modèle est un concept critique — un contrat entre l’utilisateur et l’implémenteur — et doit être soigneusement conçu.
Les objets fonction peuvent porter plus d’informations à travers une interface qu’un simple pointeur vers une fonction.
En général, passer des objets fonction donne de meilleures performances que passer des pointeurs vers des fonctions.
bool greater(double x, double y) { return x > y; }
sort(v, greater); // pointeur vers une fonction : potentiellement lent
sort(v, [](double x, double y) { return x > y; }); // objet fonction
sort(v, std::greater{}); // objet fonction
bool greater_than_7(double x) { return x > 7; }
auto x = find_if(v, greater_than_7); // pointeur vers une fonction : inflexible
auto y = find_if(v, [](double x) { return x > 7; }); // objet fonction : transporte les données nécessaires
auto z = find_if(v, Greater_than<double>(7)); // objet fonction : transporte les données nécessaires
Vous pouvez, bien sûr, généraliser ces fonctions en utilisant auto ou des concepts. Par ex.:
auto y1 = find_if(v, [](totally_ordered auto x) { return x > 7; }); // impose un type ordonné
auto z1 = find_if(v, [](auto x) { return x > 7; }); // espère que le type supporte `>`.
Les lambdas génèrent des objets fonction.
L’argument de performance dépend du compilateur et de l’optimiseur.
Gardez les interfaces simples et stables.
Considérons un sort instrumenté avec un support de débogage simplifié :
void sort(sortable auto& s) // trier la séquence s
{
if (debug) cerr << "enter sort( " << s << ")\n";
// ...
if (debug) cerr << "exit sort( " << s << ")\n";
}
Devrait-il être réécrit ainsi ?
template<sortable S>
requires Streamable<S>
void sort(S& s) // trier la séquence s
{
if (debug) cerr << "enter sort( " << s << ")\n";
// ...
if (debug) cerr << "exit sort( " << s << ")\n";
}
Après tout, il n’y a rien dans sortable qui exige une prise en charge iostream.
D’un autre côté, il n’y a rien dans l’idée fondamentale de tri qui dit quoi‑que‑que sur le débogage.
Si nous exigeons qu’une opération soit incluée dans la vérification des concepts, l’interface devient instable : t. al. à chaque modification des facilités de collecte de données, tests, etc., la définition du modèle devra changer et chaque usage devra être recompilé. Cela est gênant et parfois impossible dans certains environnements.
Inversement, si nous utilisons une opération dans l’implémentation qui n’est pas garantie par le contrôle de concept, nous pourrions obtenir une erreur de compilation tardive.
En ne vérifiant pas les concepts pour les propriétés qui ne sont pas considérées essentielles, nous reportons la vérification jusqu’au moment de l’instanciation. Nous considérons que c’est un compromis valable.
Notez que l’utilisation de noms non locaux, non dépendants (tels que debug et cerr) introduit également des dépendances de contexte qui peuvent entraîner des erreurs « mystérieuses ».
Il peut être difficile de décider quelles propriétés d’un type sont essentielles ou non.
???
Amélioration de la lisibilité. Masquage de la mise en œuvre.
Notez que les alias de modèle remplacent de très nombreuses utilisations de traits de calcul de type.
Ils peuvent également être utilisés pour embrasser un trait.
template<typename T, size_t N>
class Matrix {
// ...
using Iterator = typename std::vector<T>::iterator;
// ...
};
Cela évite à l’utilisateur de Matrix de savoir comment les éléments sont stockés dans un vector et économise également le fait de devoir taper à chaque fois typename std::vector<T>::.
template<typename T>
void user(T& c)
{
// ...
typename container_traits<T>::value_type x; // mauvais, verbeux
// ...
}
template<typename T>
using Value_type = typename container_traits<T>::value_type;
Cela évite à l’utilisateur de Value_type de devoir connaître la technique utilisée pour implémenter value_type.
template<typename T>
void user2(T& c)
{
// ...
Value_type<T> x;
// ...
}
Une utilisation simple courante pourrait être exprimée : « Emballer les traits ! »
typename hors des déclarations using.using à typedef pour définir des aliasLisibilité améliorée : avec using, le nouveau nom apparaît en premier plutôt qu’un élément du corps d’une déclaration.
Généralité : using peut être utilisé pour les alias de modèle, alors que typedef ne peut pas facilement les faire.
Uniformité : using a une syntaxe similaire à auto.
typedef int (*PFI)(int); // OK, mais complexe
using PFI2 = int (*)(int); // OK, préféré
template<typename T>
typedef int (*PFT)(T); // erreur
template<typename T>
using PFT2 = int (*)(T); // OK
typedef. Cela donnera beaucoup de « hits » :-(Écrire les types d’arguments de modèle explicitement peut être fastidieux et inutilement verbeux.
tuple<int, string, double> t1 = {1, "Hamlet", 3.14}; // type explicite
auto t2 = make_tuple(1, "Ophelia"s, 3.14); // meilleur ; type déduit
Notez l’utilisation du suffixe s pour garantir que la chaîne est un std::string, plutôt qu’une chaîne C‑style.
Comme vous pouvez écrire un make_T de façon triviale, le compilateur pourrait l’implémenter; les fonctions make_T deviennent donc redondantes à l’avenir.
Parfois, il n’y a pas de bonne façon de faire déduire les arguments de modèle et parfois, vous voulez les spécifier explicitement :
vector<double> v = { 1, 2, 3, 7.9, 15.99 };
list<Record*> lst;
Notez que C++17 rend cette règle obsolète en permettant la déduction de paramètres de modèle directement depuis les paramètres de constructeur : Template parameter deduction for constructors (Rev. 3).
Par ex. :
tuple t1 = {1, "Hamlet"s, 3.14}; // déduit : tuple<int, string, double>
Un argument de modèle sans contrainte est une correspondance parfaite pour tout, de sorte que ce modèle peut être préféré à des types plus spécifiques qui nécessitent des conversions mineures.
Cela est particulièrement gênant/danger chez l’utilisation d’ADL.
Les noms communs augmentent ce risque.
namespace Bad {
struct S { int m; };
template<typename T1, typename T2>
bool operator==(T1, T2) { cout << "Bad\n"; return true; }
}
namespace T0 {
bool operator==(int, Bad::S) { cout << "T0\n"; return true; } // comparer à int
void test()
{
Bad::S bad{ 1 };
vector<int> v(10);
bool b = 1 == bad;
bool b2 = v.size() == bad;
}
}
Cela imprime T0 et Bad.
Maintenant, l’opérateur == dans Bad était conçu pour créer des problèmes, mais auriez‑vous remarqué ce problème en production ?
Le problème est que v.size() renvoie un entier non signé, alors qu’une conversion est nécessaire pour appeler l’propre == ; l’opérateur == dans Bad ne nécessite aucune conversion.
Les types réalistes, comme les itérateurs de la bibliothèque standard, peuvent présenter des tendances anti‑sociales similaires.
Si un modèle sans contrainte est défini dans le même espace de nom qu’un type, ce modèle sans contrainte peut être trouvé par ADL (tel qu’il se passe dans l’exemple).
Ou soit‑tout, il est très visible.
Cette règle devrait ne pas être nécessaire, mais le comité ne peut pas accepter d’exclure l’ADL pour des modèles sans contrainte.
Malheureusement, la bibliothèque standard viole largement cette règle, en plaçant de nombreux modèles sans contrainte et types dans le même espace de nom std.
enable_ifUne façon optimale de contourner l’absence de support direct de concepts.
enable_if peut être utilisé pour conditionner la définition des fonctions et pour choisir parmi une série de fonctions.
template<typename T>
enable_if_t<is_integral_v<T>>
f(T v)
{
// ...
}
Équivalent à :
template<Integral T>
void f(T v)
{
// ...
}
Soyez attentif aux contraintes complémentaires.
Simuler la surcharge de concept avec enable_if force parfois l’usage de cette technique d’erreur.
???
L’injection de type introduit un niveau supplémentaire d’indirection en cachant les informations de type derrière une frontière de compilation séparée.
???
Exceptions : l’injection de type est parfois appropriée, comme pour std::function.
???
Une définition de modèle (classe ou fonction) peut contenir du code arbitraire, donc une révision complète des techniques de programmation C++ couvrirait donc le sujet.
Cependant, ce section se concentre sur ce qui est spécifique à la mise en œuvre d’un modèle.
En particulier, elle se concentre sur la dépendance d’une définition de modèle à son contexte.
Plus de compréhension. Réduction des erreurs dues à des dépendances inattendues. Facilité d’outils.
template<typename C>
void sort(C& c)
{
std::sort(begin(c), end(c)); // dépendance nécessaire et utile
}
template<typename Iter>
Iter algo(Iter first, Iter last)
{
for (; first != last; ++first) {
auto x = sqrt(*first); // dépendance potentiellement surprenante : quel sqrt() ?
helper(first, x); // dépendance potentiellement surprenante :
// helper est choisi en fonction de first et x
TT var = 7; // dépendance potentiellement surprenante : quel TT ?
}
}
Les modèles apparaissent généralement dans des fichiers d’en‑tête, leurs dépendances de contexte sont donc plus vulnérables aux dépendances d’ordre d’#include que les fonctions dans des fichiers .cpp.
Avoir un modèle qui opère uniquement sur ses arguments serait un moyen de réduire le nombre de dépendances au minimum, mais ce serait généralement ingérable.
Par ex. : les algorithmes utilisent souvent d’autres algorithmes et invoquent des opérations qui ne sont pas exclusivement définies sur les arguments.
Et ne parlons même pas des macros !
Voir aussi : T.69
??? Tricky
Un membre qui ne dépend pas d’un paramètre de modèle ne peut être utilisé que pour un argument de modèle spécifique.
Cela limite l’utilisation et augmente généralement la taille du code.
template<typename T, typename A = std::allocator<T>>
// requires Regular<T> && Allocator<A>
class List {
public:
struct Link { // ne dépend pas d’A
T elem;
Link* pre;
Link* suc;
};
using iterator = Link*;
iterator first() const { return head; }
// …
private:
Link* head;
};
List<int> lst1;
List<int, My_allocator> lst2;
Ceci semble inoffensif, mais Link dépend formellement de l’allocateur (même s’il ne l’utilise pas).
Cela provoque des instantiations redondantes qui peuvent être surprenantes.
La solution habituelle consiste à faire de cette classe interne une classe séparée (en dehors du modèle) avec son propre ensemble minimal de paramètres de modèle.
template<typename T>
struct Link {
T elem;
Link* pre;
Link* suc;
};
template<typename T, typename A = std::allocator<T>>
// requires Regular<T> && Allocator<A>
class List2 {
public:
using iterator = Link<T>*;
iterator first() const { return head; }
// …
private:
Link<T>* head;
};
List2<int> lst1;
List2<int, My_allocator> lst2;
Certains trouvent l’idée que le Link n’est plus caché dans la liste très effrayante, nous l’appelons donc SCARY.
Il est mentionné que l’acronyme SCARY décrit les déclarations qui semblent erronées (apparaient contrainte par l’implémentation), mais qui fonctionnent réellement grâce à la bonne implémentation.
Cela s’applique également aux lambdas qui ne dépendent pas de tous les paramètres de modèle.
Autorise les membres de la classe de base à être utilisés sans spécifier de paramètres de modèle et sans instanciation du modèle.
template<typename T>
class Foo {
public:
enum { v1, v2 };
// …
};
???
struct Foo_base {
enum { v1, v2 };
// …
};
template<typename T>
class Foo : public Foo_base {
public:
// …
};
Une version plus générale de cette règle serait : « Si un membre de classe dépend de seulement N paramètres de modèle sur M, placez‑le dans une classe de base contenant seulement N paramètres ».
Pour N = 1, on peut choisir une classe de base adjacente à cette classe dans le même espace de nom, comme dans T.61.
??? Que faire des constantes ? des statiques de classe ?
Un modèle définit une interface générale.
La spécialisation offre un mécanisme puissant pour fournir des implémentations alternatives de cette interface.
???
Références : ?
???
C’est une version simplifiée de std::copy (en oubliant la possibilité de séquences non contiguës).
struct trivially_copyable_tag {};
struct non_trivially_copyable_tag {};
// T n’est pas trivially copyable
template<class T> struct copy_trait { using tag = non_trivially_copyable_tag; };
// int est trivially copyable
template<> struct copy_trait<int> { using tag = trivially_copyable_tag; };
template<class Iter>
Out copy_helper(Iter first, Iter last, Iter out, trivially_copyable_tag)
{
// use memmove
}
template<class Iter>
Out copy_helper(Iter first, Iter last, Iter out, non_trivially_copyable_tag)
{
// use loop calling copy constructors
}
template<class Iter>
Out copy(Iter first, Iter last, Iter out)
{
using tag_type = typename copy_trait<std::iter_value_t<Iter>>::tag;
return copy_helper(first, last, out, tag_type{});
}
void use(vector<int>& vi, vector<int>& vi2, vector<string>& vs, vector<string>& vs2)
{
copy(vi.begin(), vi.end(), vi2.begin()); // uses memmove
copy(vs.begin(), vs.end(), vs2.begin()); // uses a loop calling copy constructors
}
C’est une technique générale et puissante pour la sélection d’algorithme à la compilation.
Avec les contraintes C++20, ces alternatives peuvent être distinguées directement :
template<class Iter>
requires std::is_trivially_copyable_v<std::iter_value_t<Iter>>
Out copy_helper(In, first, In last, Out out)
{
// use memmove
}
template<class Iter>
Out copy_helper(In, first, In last, Out out)
{
// use loop calling copy constructors
}
???
???
???
???
{} plutôt que () dans les modèles pour éviter les ambiguïtés() est vulnérable aux ambiguïtés grammaticales.
template<typename T, typename U>
void f(T t, U u)
{
T v1(T(u)); // erreur : v1 est considered a function, not a variable
T v2{u}; // clair : soit une variable
auto x = T(u); // vague : construction ou cast ?
}
f(1, "asdf"); // mauvais : cast from const char* to int
()Il existe trois grandes façons de laisser le code appelant personnaliser un modèle.
template<class T>
// Appel d'une fonction membre
void test1(T t)
{
t.f(); // exiger T fournit f()
}
template<class T>
void test2(T t)
// Appel d’une fonction non membre sans qualification
{
f(t); // exiger f(/T/) disponible dans le scope appelant ou dans le namespace de T
}
template<class T>
void test3(T t)
// Invocation d'une « trait »
{
test_traits<T>::f(t); // exiger la spécialisation de test_traits<> pour récupérer les fonctions/types non par défaut
}
Une trait est généralement une alias de type pour calculer un type, une fonction constexpr pour calculer une valeur ou une traditionnelle trait template à spécialiser sur le type de l’utilisateur.
Si vous voulez appeler votre propre helper helper(t) pour une valeur t qui dépend d’un paramètre de type de modèle, mettez‑le dans un ::detail namespace et qualifiez l’appel comme detail::helper(t);.
Une appel non qualifié devient un point de personnalisation où toute fonction helper dans le namespace de t peut être invoquée; cela peut cause des problèmes comme l’appel de fonctions non contraintes par inadvertance.
Les modèles sont le pilier du support C++ pour la programmation générique et les hiérarchies de classes le pilier du support de la programmation orientée objet.
Les deux mécanismes de langage peuvent être utilisés efficacement en combinaison, mais quelques pièges de conception doivent être évités.
Templatiser une hiérarchie de classe qui possède de nombreuses fonctions, en particulier beaucoup de fonctions virtuelles, peut entraîner un gonflement du code.
template<typename T>
struct Container { // interface
virtual T* get(int i);
virtual T* first();
virtual T* next();
virtual void sort();
};
template<typename T>
class Vector : public Container<T> {
public:
// …
};
Vector<int> vi;
Vector<string> vs;
Il est probablement mauvais de définir sort comme membre d’une classe conteneur, mais ce n’est pas rare et c’est un bon exemple de ce qu’il ne faut pas faire.
Dans ce cas, le compilateur ne sait pas si Vector<int>::sort() est appelé, il doit donc générer le code pour cette fonction.
Même si ces deux fonctions ne sont jamais appelées, elles entraîneront une augmentation de la taille du code.
Imaginez si une hiérarchie de classe contient des dizaines de fonctions membres et de dizaines de classes dérivées avec beaucoup d’instanciations.
Dans de nombreux cas, vous pouvez fournir une interface stable sans paramétriser la base ; voir « base stable » (Stable base) et « Object‑oriented and GP » (OO and GP)
Un tableau de dérivés peut implicitement « démouler » vers un pointeur de base avec des résultats potentiellement désastreux.
Supposons que Apple et Pear sont deux types de Fruit.
void maul(Fruit* p)
{
*p = Pear{}; // mettre un Pear dans *p
p[1] = Pear{}; // mettre un Pear dans p[1]
}
Apple aa [] = { an_apple, another_apple }; // aa contient Apples (évidemment !)
maul(aa);
Apple& a0 = &aa[0]; // un Pear ?
Apple& a1 = &aa[1]; // un Pear ?
Probablement, aa[0] sera un Pear (sans cast).
Si sizeof(Apple) != sizeof(Pear) la conservation de aa[1] ne sera pas alignée sur le commencement d’un objet dans le tableau.
Cela entraîne une violation de type et potentiellement une corruption mémoire.
Ne jamais écrire ce code.
Notez que maul() viole la règle [T* pointe vers un objet individuel](#rf-ptr).
Alternative : utiliser un conteneur correctement templatisé :
void maul2(Fruit* p)
{
*p = Pear{}; // mettre un Pear dans *p
}
vector<Apple> va = { an_apple, another_apple }; // vaut contient Apples (évidemment !)
maul2(va); // erreur : ne peut convertir vector<Apple> en Fruit*
maul2(&va[0]); // vous l’avez demandé
Apple& a0 = &va[0]; // un Pear ?
Notez que l’affectation dans maul2() violé la règle de non‑slicing.
???
???
???
C++ ne supporte pas cela.
S’il le faisait, les tables virtuelles ne pourraient pas être générées à la compilation.
D’autre part, les implémentations doivent gérer le linking dynamique.
class Shape {
// …
template<class T>
virtual bool intersect(T* p); // erreur : le modèle ne peut être virtuel
};
Nous avons besoin d’une règle parce que les gens continuent de demander.
Double dispatch, visitors, calculer quelle fonction appeler.
Le compilateur gère cela.
Améliorer la stabilité du code.
Éviter le gonflement du code.
C’est peut‑être une classe de base.
struct Link_base { // stable
Link_base* suc;
Link_base* pre;
};
template<typename T> // wrapper templatisé pour ajouter la sûreté de type
struct Link : Link_base {
T val;
};
struct List_base {
Link_base* first; // premier élément (si existant)
int sz; // nombre d’éléments
void add_front(Link_base* p);
// …
};
template<typename T>
class List : List_base {
public:
void put_front(const T& e) { add_front(new Link<T>{e}); } // cast implicite à Link_base
T& front() { return static_cast<Link<T>*>(first)->val; } // cast explicite vers Link<T>
// …
};
List<int> li;
List<string> ls;
Il n’y a maintenant qu’une copie des opérations de lien et de déconnexion d’une List.
Les classes Link et List ne font que manipuler les types avec sécurité.
Au lieu d’utiliser une class de base séparée, une technique courante est de spécialiser pour void ou void* et de faire en sorte que le modèle générique pour T ne soit qu’une série de casts sécurisés vers/depuis l’implémentation centrale void.
Alternative : utiliser un implémentation Pimpl.
???
…
Les modèles variadiques sont le mécanisme le plus général pour cela, et ils sont efficaces et type‑sûrs. Fréquenter les variadic C vararg.
??? printf
va_arg dans le code utilisateur.???
??? attention aux arguments de type move‑only et référence
???
???
??? forwarding, type checking, references
???
Il existe des moyens plus précis de spécifier une séquence homogène, comme un initializer_list.
???
???
Les modèles offrent un mécanisme général pour la programmation à la compilation.
The rest of the document continues similarly.
(Note: This translation covers the entire provided snippet, preserving formatting, code indentation at 4 spaces, and adding @TODO-LINK next to missing anchor references such as #gsl-guidelines-support-library, #res-slice, #rt-regular, and placeholders like #rt-??.)