L'idée de concept (ou de contrainte)

Important : ce qui suit, bien que digne d'intérêt, doit être révisé significativement car les travaux sur les concepts sont activement en cours et nous approchons de ce qui sera, on peut le croire, un élément clé C++ pour longtemps. La spécification technique en vue de C++ 17 a bel et bien été ratifiée. Votre humble serviteur en est absolument ravi! https://botondballo.wordpress.com/2015/07/22/its-official-the-concepts-ts-has-been-voted-for-publication/ Cependant, l'intégration formelle des concepts dans le standard de C++ en tant que tel attendra au moins C++ 20.

Si vous souhaitez lire les textes échangés dans le cadre du développement du standard, allez ici.

Chaque gain d'abstraction augmente la capacité d'expression des individus et mène à repenser les techniques et les philosophies qui nous accompagnent au quotidien, dans notre travail comme dans notre vie.

Souvent, en programmation, gagner en abstraction entraîne le développement de nouvelles techniques et résulte, contrairement à ce que certains croiraient, en des programmes plus rapides et plus compacts. La mauvaise presse que subissent parfois les idées novatrices (incluant la POO, que l'ont tient pour acquis aujourd'hui mais qui a radicalement transformé la programmation) tient à un examen de ces idées à la lueur de leur premiers pas: les applications initiales ne sont pas toujours aussi élégantes qu'elles le pourraient, et les compilateurs peinent à traduire ces idées de manière à en tirer véritablement profit.

Une fois que les compilateurs et les optimisateurs atteignent un niveau de maturité raisonnable face à une percée conceptuelle donnée, une fois aussi que la pensée des programmeurs a intégré correctement les nouvelles idées, le monde des possibles s'en trouve grandi.

Le potentiel de généricité d'un langage OO peut s'exprimer entre autres :

Une autre catégorie, moins importante toutefois, est celle des délégués, qui est une forme de polymorphisme basée sur la signature, cousine OO des pointeurs de fonctions. C++ supporte directement cette pratique par std::function, et C# le fait depuis longtemps par delegate. J'escamote ici plusieurs thématiques propres à l'abstraction (mixin, classes anonymes, expressions λ et foncteurs pour ne nommer que celles-là) parce qu'elles importent moins pour notre propos sur cette page.

Le polymorphisme et la programmation générique sont possibles à la fois en C++, en Java et en C#, quoique la programmation générique soit plus puissante sous C++ pour diverses raisons techniques et conceptuelles.

Le polymorphisme dynamique traditionnel permet une abstraction opératoire basée sur une communauté structurelle; c'est une approche intrusive (le parent fait partie de l'enfant), mais que certaines techniques permettent de rendre extrusive. Les types auxquels s'applique le polymorphisme ont une parenté commune. Les opérations exploitant la généricité par le polymorphisme procèdent à travers un lien indirect (pointeur ou référence) sur quelque chose qui mène au moins vers ce parent commun, sollicitant des opérations que tous les enfants de ce parent ont nécessairement implémenté.

La programmation générique permet une abstraction opératoire basée sur la possibilité d'appliquer des opérations sur les types impliqués. Nul besoin a priori d'un lien de parenté entre les types impliqués. Rien n'empêche, évidemment, d'appliquer un algorithme de programmation générique sur des types polymorphiques – une application de l'abstraction n'empêche pas l'autre, après tout.

Toute technique a ses avantages et ses inconvénients.

Le polymorphisme traditionnel permet une généricité dynamique, où la méthode à invoquer sur une abstraction donnée ne sera véritablement connue qu'au moment de l'exécution d'un programme. Ce dynamisme facilite l'évolutivité des programmes et permet le développement d'outils extrêmement flexibles, s'appliquant à des types a priori inconnus. C'est ce que les anglophones nomment du Late Binding.

À travers une abstraction polymorphique, des opérations arbitrairement complexes peuvent être sollicitées de manière transparente.

Le polymorphisme et le dynamisme qu'il permet constituent la base de plusieurs schémas de conception populaires comme observateur ou proxy.

Le dynamisme du polymorphisme a un coût, par contre. Tout appel de méthode polymorphique est indirect et appelle une fonction qu'il est impossible de déterminer a priori, outre dans quelques cas très particulier, ce qui empêche les compilateurs de procéder à plusieurs optimisations qui seraient possibles sur des opérations non polymorphiques. Appeler une méthode polymorphique coûte donc plus cher en temps d'exécution qu'appeler une méthode qui ne l'est pas.

// j'utilise struct plutôt que class
// seulement pour alléger l'écriture
struct B {
   virtual int f(int) = 0;
   virtual ~B() = default;
};
struct D0 : B {
   int f(int n) const {
      return -n;
   }
};
struct D1 : B {
   int f(int n) const {
      return n * n;
   }
};
int g(B *pB) {
   return pB->f(3); // -3 ou 9?
}

Implémenter le polymorphisme sur une classe demande aussi d'y insérer un peu d'information supplémentaire pour faciliter l'aiguillage des méthodes. Cette information n'occupe pas un très grand espace, mais il est sage d'en être conscient.

Ces coûts ne sont pas prohibitifs sur la majorité des systèmes, mais peuvent l'être si les ressources sont comptées (dans un système embarqué ou en temps réel, par exemple).

La programmation générique permet une généricité statique, où les opérations à solliciter sont connues au moment où le programme est compilé. Ceci permet au compilateur de pratiquer un certain nombre d'optimisations significatives, par exemple du Method Inlining. À moins que les opérations appliquées aux types par un algorithme générique ne soient elles-mêmes polymorphiques, un bon compilateur pourra générer du code extrêmement optimisé dans le cas de programmes génériques.

Évidemment, il y a aussi un coût associé à la programmation générique. En particulier, chaque algorithme appliqué à un type donné (chaque instanciation d'un modèle générique) doit être généré pour ce type, ce qui peut entraîner une croissance importante de la taille d'un programme une fois celui-ci compilé (les sources, elles, seront plus compactes). Il y a ici encore des trucs pour s'en sortir, et les compilateurs sont devenus excellents pour optimiser au maximum le code générique.

template <class T>
   T minimum(T a, T b) {
      return a < b? a : b;
   }
#include <string>
#include <iostream>
int main() {
   using namespace std;
   int i0, i1;
   if (cin >> i0 >> i1)
      // sollicite le < entre deux int
      cout << minimum(i0, i1);
   string s0, s1;
   if (cin >> s0 >> s1)
      // sollicite le < entre deux std::string
      cout << minmum(s0, s1);
}

Grandeurs et misères de la programmation générique

L'une des difficultés qu'entraîne la programmation générique a trait à la capacité qu'on les programmeurs d'y identifier les bogues.

En effet, le compilateur ne reconnaîtra un problème dans l'application d'un algorithme générique à un type donné que lorsqu'il tentera de générer le code correspondant et constatera l'absence d'une caractéristique clé.

Dans le cas à droite, la classe Incomparable n'expose pas d'opérateur < à la fois const et bool qui serait capable de comparer deux instances de la classe Incomparable. Si nous essayons d'appliquer l'opérateur < à deux instances d'Incomparable, alors le compilateur détectera l'erreur, évidemment. Le problème, par contre, sera celui de la génération d'un message d'erreur significatif.

Ici, sur le plan du diagnostic, on s'en tirera pas trop mal : le compilateur signalera que l'opérateur < manque pour le type Incomparable dans la fonction minimum().

Règle générale, toutefois, surtout quand les algorithmes génériques s'appellent les uns les autres, un compilateur risque de devoir signaler une erreur sur une ligne obscure dans une fonction dont le programmeur n'a à peu près jamais entendu parler.

#include <iosfwd>
#include <string>
#include <iostream>
using namespace std;
//
// requiert bool operator<(T,T)
//
template <class T>
   T minimum(T a, T b) {
      return a < b? a : b;
   }
//
// bool operator<(Incomparable,Incomparable) n'existe pas
//
struct Incomparable {
   constexpr int valeur() const noexcept {
      return 3;
   }
};
int main() {
   int i0, i1;
   // sollicite « operator<(int,int) »
   if (cin >> i0 >> i1)
      cout << Min(i0, i1) << endl;
   string s0, s1;
   // sollicite le « operator<(string,string) »
   if (cin >> s0 >> s1)
      cout << Min(s0, s1) << endl;
   Incomparable ic0, ic1;
   // ne compile pas
   cout << minimum(ic0, ic1);
}

Diagnostics et généricité

Le message d'erreur peut tenir sur des pages et des pages, retraçant chacune des étapes menant au problème et signalant les causes possibles. C'est pour le moins indigeste, et des programmes (fautifs) dont la compilation génère plus de lignes de messages d'erreurs qu'il n'y a de lignes de code source dans le programme.

Pour être utile, il faudrait au contraire qu'un message d'erreur nous indique que l'algorithme générique sollicité ne peut être appliqué au type souhaité faute d'exposer un certain nombre d'opérations, et devrait même indiquer lesquelles : ici, quelque chose comme Incomparable n'est pas Ordonnancable, par exemple. En d'autres mots, on voudrait un moyen de déterminer le contrat que devra respecter un type pour qu'on puisse lui appliquer un algorithme générique donné.

Ces contrats sont déjà exprimés informellement dans la documentation du langage et des bibliothèques. Par exemple, on dira des types susceptibles d'être insérés dans un conteneur standard qu'ils doivent être Assignable, CopyConstructible (ou, depuis C++ 11, Moveable) et LessThanComparable. En d'autres termes, un tel type doit offrir un opérateur d'affectation, un constructeur par copie et un opérateur < booléen et const permettant de déterminer un ordre entre deux instances. La réflexion en ce sens repose au moins en partie sur les travaux d'Alexander Stepanov.

Ce sont là des noms donnés à des ensembles de règles implicites, qu'on demande au programmeur de respecter. Tout type se conformant à ces règles sera insérable dans un conteneur standard; si l'une de ces règles n'est pas respectée, alors la compilation générera des erreurs. Le souhait de tous et chacun est de faire en sorte que le contrat déterminé par ces règles soit plus explicite et qu'un compilateur puisse le valider plus clairement.

La stratégie « manuelle »

Il est possible depuis longtemps de mettre en place un contrat plus formel par une saine discipline de programmation. Par exemple, si les contraintes d'un type T donné sont d'être Assignable, CopyConstructible et LessThanComparable, dans l'optique où on veut pouvoir insérer T dans un conteneur standard, on pourrait définir une classe ContainerInsertible comme proposée à droite.

Cette stratégie fonctionne somme toute assez bien, permet d'obtenir de meilleurs messages d'erreur et formalise par une technique le respect du contrat d'un modèle générique. Ça reste un tour de passe-passe, cela dit, et on pourrait raisonnablement espérer quelque chose de plus formel.

template <class T>
   class ContainerInsertible {
   public:
      static void contraintes(T a) { // constructeur par copie supporté
         T b = a;
         a = b;      // opérateur d'affectation supporté
         if (a < b); // comparaison avec < supportée
      }
      ContainerInsertible() {
         // On déclare un pointeur de fonction sur la méthode de classe dans
         // laquelle on vérifie les contraintes. Attention: on n'appelle pas
         // cette fonction, mais on s'assure ainsi que le code de la méthode
         // ContainerInsertible::contraintes() soit généré pour notre type T.
         //
         // Si une partie du contrat n'est pas respectée, alors une erreur de
         // compilation sera générée à la ligne appropriée de cette méthode.
         void (*p)(T) = contraintes;
      }
   };
// Présumant que ChicConteneur soit un conteneur respectant les règles des
// conteneurs standard, on voudra que le type T de ses éléments soit conforme
// à la règle définie par ContainerInsertible.
template <class T>
   class ChicConteneur {
      // Ce membre privé a pour rôle de forcer l'instanciation des contraintes
      // et, au besoin, de stopper la compilation. Si on rédigeait une fonction,
      // on aurait simplement déclaré un ContainerInsertible<T>() sans nom
      ContainerInsertible <T> nePasToucher__;
      // reste du conteneur
   };

Stratégie manuelle (raffinement)

Herb Sutter, dans More Exceptional C++, met en relief une vision encore plus charmante de cette stratégie. Cette version a été proposée par Bjarne Stroustrup avec contributions d'Alex Stepanov et de Jeremy Siek.

La nuance de cette proposition est de reconnaître que dans un cas comme ceux de ChicConteneur<T> et de ContainerInsertible<T>, on pourrait affirmer que ChicConteneur<T> « contient » intrinsèquement, et pas seulement par composition, la contrainte ContainerInsertible<T>.

On donc fait face à un beau cas d'héritage privé (mais pas d'héritage public: il n'y a pas lieu de regrouper ensembles les ContainerInsertible<T> ou encore de les manipuler de manière polymorphique).

Profitant de cette intuition, le code devient :

template <class T>
   class ContainerInsertible {
      static void contraintes(T a) { // constructeur par copie supporté
         T b = a;
         a = b;      // opérateur d'affectation supporté
         if (a < b); // comparaison avec < supportée
      }
   protected:
      ContainerInsertible() {
         void (*p)(T) = contraintes;
      }
   };
template <class T>
   class ChicConteneur : ContainerInsertible<T> {
      // Manière alternative d'indiquer que ChicConteneur<T> a un
      // sens qui dépend de la validité de ContainerInsertible<T>
      // reste du conteneur
   };

Concept de... concept

Un mécanisme pour déterminer des contrats sur des types génériques à même le langage a été adopté en 2015 sous la forme d'une spécification technique en vue d'une intégration dans C++ 17. Ce mécanisme est ce qu'on nomme les concepts.

Dans les mots d'Alexander Stepanov, tirés d'une entrevue de 2015 :

« Concepts are requirements on types; preconditions are requirements on values. A concept might indicate that a type of a value is some kind of integer. A precondition might state that a value is a prime number. »

Dans les mots d'Andrew Sutton, dans une présentation donnée à C++ Now 2015 (je paraphrase), un concept est un prédicat qui détermine l'appartenance d'un type à un ensemble de types, un peu à la manière de certains traits. Ceci inclut la vérification de certaines expressions sur le plan syntaxique comme sur le plan sémantique. Les traits se limitent aux types et aux valeurs, mais ne permettent pas de vérifier la présence d'un template; les concepts, eux., vont beaucoup plus loin.

Ainsi, on souhaite pouvoir remplacer le code ci-dessous... ... par quelque-chose qui ressemblerait au code ci-dessous...
template <class It, class T>
   It trouver(It debut, It fin, const T &elem) {
      for (; debut != fin; ++debut)
         if (*debut == elem) return debut;
      return fin;
   }
template <ForwardIterator It, Comparable T>
   It trouver(It debut, It fin, const T &elem) {
      for (; debut != fin; ++debut)
         if (*debut == elem) return debut;
      return fin;
   }

Remarquez les qualifications remplaçant le mot générique class dans la spécification des types auxquels s'applique l'algorithme générique. Ce ne sont pas des types mais des concepts, donc des règles pouvant être appliquées à des types qui ne sont pas nécessairement apparentés.

On vise donc introduire le mot clé concept dans le langage C++ à partir de C++ 17. Ce mot aura un usage semblable à celui du mot class, et sera paramétrique au sens des templates, mais permettra d'indiquer les opérations que doit supporter tout type respectant le contrat défini par le concept.

En identifiant clairement les attendus des divers types, l'un des principaux objectifs de la démarche sous-jacente aux concepts est de mener à une meilleure génération de messages d'erreurs (très complexes à bien générer avec les templates, de par leur nature).

Les concepts de C++ 17

J'ai eu le plaisir de participer aux débats le formalisant lors d'une rencontre de WG21 à Lenexa, en 2015. Les concepts sont maintenant une spécification technique en vue de C++ 17 , pour fins d'expérimentation. Ce qui suit peut donc changer, mais fera pour l'essentiel sous peu officiellement partie du langage (à moins d'une catastrophe).

Après plusieurs bouleversements et tergiversations, nous avons maintenant une idée assez précise de ce que seront formellement les concepts dans C++ à partir de C++ 17.

Utiliser un concept

Pour illustrer le propos, examinons tout d'abord comment utiliser un concept un peu bidon que nous nommerons PrefixIncrementable et qui s'avèrera pour tout type T tel que si e est de type T, alors ++e sera légal – notez immédiatement que c'est un concept d'une utilité plus que discutable, mais je l'utiliserai à titre illustratif.

Nous montrerons d'abord comment utiliser ce concept, puis nous verrons comment le définir.

Imaginons maintenant une fonction générique prochain(T) telle que celle proposée à droite. Cette fonction est simple, pour ne pas dire simpliste, mais sollicite le constructeur de copie de T, son destructeur, de même que son opérateur ++ préfixé (je n'ai pas utilisé ici le fait que ++p devrait normalement être de type T&, mais on aurait bien sûr pu le faire).

template <class T>
   T prochain(T p) {
      return ++p;
   }

Si l'on suppose les concepts susmentionnés, alors on pourrait écrire la même fonction comme proposé à droite. Ici, la clause requires pourrait composer plusieurs concepts sous une forme d'expressions logiques, le tout étant évalué à la compilation, sur la base des types. Cette forme est verbeuse mais permet d'exprimer des idées complexes sur la base de concepts composites.

template <class T>
   requires PrefixIncrementable<T>
T prochain(T p) {
   return ++p;
}

Une autre écriture, plus concise mais tout à fait équivalente dans ce cas-ci, serait celle proposée à droite. Elle ne se prête pas à la composition de concepts mais est plus directe.

template <PrefixIncrementable T>
T prochain(T p) {
   return ++p;
}

La version à droite est encore plus directe, et permet de constater qu'il est possible de surcharger des fonctions sur la base de concepts.

Notez le type de retour de cette dernière version, qui est lui-même un concept. On pourrait appeler cette fonction dans une autre fonction telle que :

template <class T>
   void g(T p) {
      auto q0 = prochain(p); // Ok
      PrefixIncrementable q = prochain(p); // Ok; constrained auto
   }

Les concepts permettent de contraindre ce qu'il est possible de faire sur le plan opératoire à partir des types retournés par les fonctions, mais de manière non-intrusive.

PrefixIncrementable prochain(PrefixIncrementable p) {
   return ++p;
}

Définir un concept

Un concept est une constante booléenne générique ou un prédicat statique générique, selon le cas. Pour PrefixIncrementable, nous souhaiterions sans doute un prédicat testant le support, par le type visé, de l'opérateur ++ préfixé. L'écriture serait alors :

template <class T>
   concept bool PrefixIncrementable = requires(T e) { ++e; };

La clause requires pourrait exprimer plusieurs contraintes opératoires, qui ne sont pas évaluées ici (il n'y a pas vraiment d'objet e, et le concept n'est pas une fonction). Si nous avions voulu exprimer que ++e doive retourner un T& ou quelque chose de convertible en T&, nous aurions aussi pu écrire :

template <class T>
   concept bool PrefixIncrementable = requires(T e) {
      { ++e } -> T&;
   };

Pour un concept plus simple, comme PasTropGros qui s'avèrerait quand un T occupe au plus deux bytes en mémoire, nous choix seraient les suivants :

Notation « fonction » Notation « fonction » (bis.) Notation « constante »
template <class T>
   concept bool PasTropGros(T e) {
      return sizeof(e) <= 2;
   };
template <class T>
   concept bool PasTropGros() {
      return sizeof(T) <= 2;
   };
template <class T>
   concept bool PasTropGros = sizeof(T) <= 2;

Ces notations sont complémentaires, et le choix en est surtout un de confort. Un concept peut s'exprimer sur la base de plus d'un type, de constantes entières, de variadiques... tout comme l'ensemble des templates. Par exemple :

template <class ... Ts>
   concept bool SontPasTropGros {
      return PasTropGros(Ts)...; // première notation ci-dessus
   };

Une notation concise pour tenir compte des concepts est ce qu'on appelle l'introduction de paramètres, par souci de concision. Ainsi, les deux écritures ci-dessous sont équivalentes :

template <class T, class U>
    concept bool ZeConcept = true;
template <class A, class B>
   requires ZeConcept<A,B>
   struct ZeStruct {
      // ...
   };
template <class T, class U>
    concept bool ZeConcept = true;
ZeConcept{A,B} struct ZeStruct {
   // ...
};

Il en va de même pour les suivantes :

template <class T, int N, class ...Ts>
    concept bool ZeConcept = true;
template <class A, class ... B>
   requires ZeConcept<A,3,B...>
   struct ZeStruct {
      // ...
   };
template <class T, int N, class ...Ts>
    concept bool ZeConcept = true;
ZeConcept{A,3,...B} struct ZeStruct {
   // ...
};

Lectures complémentaires

Pour une liste des concepts standards de C++, voir http://en.cppreference.com/w/cpp/concept

À ma connaissance, la version la plus récente du Working Draft est http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4630.pdf

Le concept de concept est une idée des plus fécondes, et la syntaxe proposée évolue rapidement.

Je vous invite à lire à ce sujet l'article http://public.research.att.com/~bs/popl06.pdf,  de même qu'un document de grande qualité par plusieurs des plus importants penseurs du domaine : http://www.cs.colorado.edu/~siek/pubs/pubs/2006/gregor06:_concepts.pdf.

Analyse sémantique des templates de C++, par Jeremy Siek et Walid Taha en 2006 : http://www.cs.colorado.edu/~siek/pubs/pubs/2006/siek06:_sem_cpp.pdf

Les Concept Maps, par Jeremy Siek en 2006 : http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2098.pdf

Pour mieux comprendre l'utilité des concepts, voir cet exercice de Fernando Pelliccioni en 2014 qui explique comment exprimer une fonction a priori simple comme min(a,b) :

Les concepts sans les concepts :

Comment en arriver à quelque chose qui ressemble à des concepts, mais avec std::enable_if, un texte de Paul Fultz II en 2014 : http://pfultz2.com/blog/2014/08/17/type-requirements/

Pour quelques documents un peu plus anciens mais tout aussi intéressants :

Pour suivre l'évolution du dossier  :

Les concepts étant essentiellement prêts à être formellement intégrés au langage, il faut maintenant baliser le chemin vers une bibliothèque standard enrichie de concepts. Texte de Matt Austern, Gabriel Dos Reis, Eric Niebler, Bjarne Stroustrup, Herb Sutter, Andrew Sutton et Jeffrey Yasskin en 2014 : http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4263.pdf

Les raisons de souhaiter les concepts, par Bjarne Stroustrup en 2016 : https://isocpp.org/blog/2016/02/a-bit-of-background-for-concepts-and-cpp17-bjarne-stroustrup

Explication de ce que sont les concepts, par Andrew Sutton en 2015 : http://accu.org/index.php/journals/2157

Abuser des concepts pour réaliser des calculs quelconques, patr Jonathan Müller en 2016 : http://foonathan.github.io/blog/2016/03/10/computational-concepts.html

Alléger l'écriture de concepts, proposition de Zhiaho Yuan en 2016 : http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0324r0.html (un texte intéressant qui soulève des questions subtiles)

En 2015, Manu Sánchez discute de ce qu'il qualifie de « concepts conviviaux » (une bibliothèque de son cru) avec C++ 14 : http://manu343726.github.io/2015/08/01/user-friendly-cpp14-concepts.html

Textes de Tom Honermann :

En 2016, Jonathan Müller fait une émulation syntaxique avec C++ 14 (et un soupçon de C++ 17) des concepts : https://foonathan.github.io/blog/2016/09/09/cpp14-concepts.html

En 2016, Andrzej Krzemieński compare enable_if et les concepts : https://akrzemi1.wordpress.com/2016/09/21/concepts-lite-vs-enable_if/

Texte d'Isabella Muerte en 2016 qui exprime quelque chose qui se rapproche des concepts à partir de C++ 14 (et un soupçon de C++ 17, mais qui n'est pas essentiel au propos) : http://izzys.casa/posts/implementing-concepts-in-cxx.html

Examiner le sens de f(Concept,Concept), pour éviter que les deux paramètres ne soient présumés être du même type (ce qui serait plus restrictif que « supportant le même concept »), une proposition de Tony van Eerd et Botond Ballo en 2016 :

En 2016, Nick Athanasiou montre comment profiter de concepts pour élaborer des quantificateurs logiques : https://ngathanasiou.wordpress.com/2016/12/18/quantifiers-metaprogramming-and-concepts/

Texte de Bjarne Stroustrup en 2016, qui met de l'avant comment concevoir et bien utiliser des concepts pertinents : http://www.stroustrup.com/good_concepts.pdf

Textes de Rainer Grimm :

Expérimenter avec les concepts en utilisant Visual Studio, par Andrew Pardoe en 2017 : https://blogs.msdn.microsoft.com/vcblog/2017/02/22/learn-c-concepts-with-visual-studio-and-the-wsl/

En 2017, Jeff Trull essaie de combiner politiques et concepts : http://jefftrull.github.io/c++/eigen/csparse/suitesparse/concepts/2017/03/06/policies-and-concepts.html

Décrire un concept pour une fonction acceptant en paramètres des objets appelables, texte de 2017 par Ivan Čukić : http://cukic.co/2017/03/23/cxx-concepts-for-receiving-functions/


[1] Source : http://www.informit.com/content/images/art_stroustrup_2005/elementLinks/DnE2005.pdf, où Stroustrup définit ce que pourrait être le concept applicable à Forward_Iterator.


Valid XHTML 1.0 Transitional

CSS Valide !