IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez C++ Discussion :

[C++][Article] implementation des concepts à l'aide de templates


Sujet :

Contribuez C++

  1. #1
    Membre confirmé
    Homme Profil pro
    Développeur .NET/C/C++
    Inscrit en
    Septembre 2007
    Messages
    71
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur .NET/C/C++
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Septembre 2007
    Messages : 71
    Par défaut [C++][Article] implementation des concepts à l'aide de templates
    Bonjour à tous. Voici un petit texte issue d'une reflexion personnelle. Merci de me dire ce que vous en pensez

    Comme beaucoup d'entre vous le savent probablement, la prochaine révision du C++ devrait ajouter au langage la notion de concepts, laquelle consite à définir une interface pour chaque type afin de pouvoir vérifier au moment de la compilation si des paramètres templates possèdent bien toutes les fonctions nécessaires, et le cas échéans devrait permettre de générer des erreurs plus explicite que ce que l'on a actuellement.
    Au départ, j'ai trouvé cet ajout au langage très intéressant. Cependant, je me suis rendu compte récemment (et un peu par hazard) que même si ce principe de vérification était très utile, il n'est pas forcément nécessaire d'ajouter de nouveaux mots-clés et cette notion de concepts, car en fait, il est (sauf erreur de ma part) tout à fait possible d'avoir le même résulat en utilisant les templates tels qu'ils existent actuellement, et en garadant à peu de chose près la même syntaxe que celle introduite par les concepts. En voici le principe:

    L'idée de base des concepts est de créer à l'aide du mot-cle concept une sorte d'interface à laquelle doit satisfaire un type donné.
    exemple (inspiré d'un exemple du pdf suivant):
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    concept InputIterator<typename Iter, typename Value>{
    Iter& operator=(Iter&, const Iter& iter);
    Value operator*(const Iteré iter);
    Iter& operator++(Iter& iter);
    bool operator==(const Iter&, const Iter&);
    //...
    }
    Ensuite, on déclare une classe template en spécifiant un concept plutot que le mot cle typename. Le concept est alors "instancié", et vérifie que le type passé en paramètre est bien conforme à ce qui est attendu au moment de son instanciation.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    template <InputIterator iterator>
    class A
    {
    //...
    iterator& begin();
    iterator& end();
    //...
    }
    Maintenant, réécrivons la chose en n'utilisant que des templates. Tout d'abord il faut déclarer une classe qui va émuler la déclaration du concept précédent:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    template<typename T>
    class InputIterator
    {
    Iter& operator=(Iter& thiz, const Iter& iter){return thiz=iter;}
    Value operator*(const Iter& iter){return *iter;}
    bool operator++(const Iter& iter){return iter++;}
    bool operator==(const Iter& thiz, const Iter& iter){return thiz==iter;}
    }
    Ensuite il faut vérifier lors de l'instaciation de la classe que le template est bien conforme à nos exigence. Certes, on ne peut le spécifier dans la déclaration du template lui-même car il faut utiliser le mot-cle typename, par contre on peut toujours forcer l'instanciation de notre concept dans la déclaration de la classe
    ... comme ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    template <typename T>
    class A
    {
    typedef InputIterator<T> iterator;
    //...
    iterator& begin();
    iterator& end();
    //...
    }
    Et voilà! La ligne
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    typedef InputIterator<T> iterator;
    à elle seule va alors se charger d'instancier la classe InputIterator, laquelle ne sert qu'à vérifier que le type passé en pramètre contient bien toutes les fonctions dont on a besoin, juste comme le fairait l'utilisation d'un concept.
    A noter au passage que certains diront probablement que l'on a un appel supplémentaire qui se fait, et que cela engendre un surcout en terme de performance. A cela je répondrai que je fais confiance à mon compilateur pour éviter ce genre de désagrément et remplacer un appel à InputIterator<T>::operator++ par un appel à T::operator++ par exemple, et ce de façon automatique. Et puis, l'implémentation que je donne de la classe InputIterator n'est qu'un exemple. Mais à priori, il doit également être possible de la définir comme étant une classe qui hérite de T, et ensuite d'utiliser la directive using afin de tester la présence de chaque opérateur plutot que de les définir à chaque fois. Cela donne alors quelque chose du style:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    template<typename T>
    class InputIterator : public T
    {
    using T::operator=;
    using t::operator*;
    using t::operator++;
    using t::operator==;
    }
    Pour info, je viens de tester cette seconde implémentation pour voir si cela était effectivement possible, et après avoir tenter d'instancier un InputIterator<string>, j' obtenu l'erreur suivante:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    tests_gcc.cpp: In instantiation of 'InputIterator<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, char>':
    tests_gcc.cpp:23:   instantiated from here
    tests_gcc.cpp:11: erreur: no members matching 'std::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator==' in 'struct std::basic_string<char, std::char_traits<char>, std::allocator<char> >'
    tests_gcc.cpp:12: erreur: no members matching 'std::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator++' in 'struct std::basic_string<char, std::char_traits<char>, std::allocator<char> >'

    En plus, cette approche devrait aussi permettre de tester la présence de constructeurs par défaut et/ou de destructeur. (a vérifier pour le destructeur, bien que l'intéret d'avoir un destructeur est moins évidente je trouve).
    On peut même aller plus loin dans ce raisonnement, car les concepts ne se limitent pas à cette déclaration mais permettent aussi d'adapter le cas en permettant de "forcer" la conformance à un concept pour un type donné. Cela ressemble en fait à ce que l'on pourrer appeler de la spécialisation de concept. Inutile de dire que ce comportement peut facilement être émuler via la spécialisation de template. De même, on peut utiliser des notions telles que l'héritage de classe pour créer une hiérarchie de concepts, qu'ils soient implémentés via la notion de concept que tend à introduire la norme C0x ou via l'utilisation de classes templates.
    Enfin, je dirai que cette approche apporte pour moi un autre avantage que celui de ne pas nécessiter la modification du langage en lui-même. En effet, via cette approche il est facile de vérifier qu'un type reçu en paramètre vérifie non pas une mais différentes interfaces. Il suffit pour cela d'ajouter autant de typedefs qu'il n'y a de vérifiactions à effectuer. En fait, il la seule chose pour laquelle je n'ai pas de solution actuellement est ce qui est appelé le "concept-based overloading", à savoir la possibilité de spécialiser une classe template en fonction de l'interface à laquelle satisfait un type donné. Mais en cherchant, il doit bien avoir moyen d'y arriver.

  2. #2
    Alp
    Alp est déconnecté
    Expert confirmé

    Avatar de Alp
    Homme Profil pro
    Inscrit en
    Juin 2005
    Messages
    8 575
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations forums :
    Inscription : Juin 2005
    Messages : 8 575
    Par défaut
    Message très très intéressant. Merci

    Plusieurs remarques :

    a - La vérification du fait que le paramètre (disons T) passé au template est conforme au concept est effective que si l'on appelle quelque part ce dont on a besoin venant de T. Non ?
    Est-ce pareil avec les concepts ?

    b - Pour le concept-based overloading, j'ai une petite bribe (ça fait pas beaucoup ) d'idée. En ajoutant un niveau de généricité entre la classe/fonction qui veut se servir du concept et le concept lui-même, il doit y avoir moyen. A méditer.

    Pour terminer : visiblement tu connais ton sujet. Tu as déjà écrit un article sur le sujet ?

  3. #3
    Membre confirmé
    Homme Profil pro
    Développeur .NET/C/C++
    Inscrit en
    Septembre 2007
    Messages
    71
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur .NET/C/C++
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Septembre 2007
    Messages : 71
    Par défaut
    Citation Envoyé par Alp Voir le message
    Message très très intéressant. Merci

    Plusieurs remarques :

    a - La vérification du fait que le paramètre (disons T) passé au template est conforme au concept est effective que si l'on appelle quelque part ce dont on a besoin venant de T. Non ?
    Est-ce pareil avec les concepts ?
    A voir les quelques tests que j'ai fait avec gcc, il semble que la vérification se fait même si tu n'accède pas aux membres du type passé en paramètre. (c'est d'ailleurs bien là l'intéret de la chose) . Par contre je n'ai pas testé avec d'autres compilateurs, à voir donc.

    b - Pour le concept-based overloading, j'ai une petite bribe (ça fait pas beaucoup ) d'idée. En ajoutant un niveau de généricité entre la classe/fonction qui veut se servir du concept et le concept lui-même, il doit y avoir moyen. A méditer.
    En fait dans la norme, ils parlent de rajouter le mot-clé where pour automatiser cela. Je pense que la meilleur solution reste l'ajout de ce mot-clé. Tout autre solution devrait s'avérer lourde à l'usage (enfin je pense).

    Pour terminer : visiblement tu connais ton sujet. Tu as déjà écrit un article sur le sujet ?
    Merci du compliment Mais je sais pas si on peut dire que je maitrise tant que ça le sujet. C'est juste que je me suis aperçu qu'en travaillant comme décrit plus haut, on en arrive au même résultat qu'avec les concepts. Pour le reste je me suis documenté au moment d'écrire mon post et j'ai aussi fait quelques tests à cette occasion. Aussi je n'est jamais écrit (et je dirai même pris le temps d'écrire) un article sur le sujet.
    Mais depuis, j'avoue que j'essaie d'utiliser cette méthode quand je développe en C++ (généralement pendant mon temps libre, on n'utilise pas C++ là ou je bosse). Et puis, je trouve que cette façon de faire non seulement permet de rajouter des vérifications au moment de la compilation du programme, mais aussi de la documenter étant donné que grâce à cela, on a une idée de ce que sont les paramètres templates que l'ont reçoit quand on retourne dans son code 3 mois après l'avoir écrit .

  4. #4
    Alp
    Alp est déconnecté
    Expert confirmé

    Avatar de Alp
    Homme Profil pro
    Inscrit en
    Juin 2005
    Messages
    8 575
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations forums :
    Inscription : Juin 2005
    Messages : 8 575
    Par défaut
    Juste une petite remarque sur ton apport d'informations : aux dernières nouvelle, c'est le mot clé requires qui a remplacé le mot clé where. Je me suis aussi pas mal documenté il y a quelques temps sur les concepts

  5. #5
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Par défaut
    Citation Envoyé par bountykiler Voir le message
    A voir les quelques tests que j'ai fait avec gcc, il semble que la vérification se fait même si tu n'accède pas aux membres du type passé en paramètre.
    Ca me semblait contraire aux règles du langage, j'ai donc fait un test pour vérifier...

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include <iostream>
     
    struct A
    {
    	int g() {return 42;}
    };
     
    template<class T> struct HasFAndG
    {
    	T &myT;
    	HasFAndG(T &t) : myT(t) {}
    	int f() {return myT.f();}
    	int g() {return myT.g();}
    };
     
    template<class T> int function(T &t) // Problème si j'ai un T const & : Il me faut un HasFAndGConst
    {
    	typedef HasFAndG<T> RealT;
    	RealT realT(t);
    	int result = realT.g();
    	// Ligne 1
    	// result+= realT.f();
    	return result;
    }
     
    int main()
    {
    	A a;
    	std::cout << "Hello world!" << std::endl;
    	std::cout << function(a) << std::endl;
    	return 0;
    }
    Tant qu'on ne décommente pas ligne 1, le code compile (test fait avec gcc, et je ne vois pas pourquoi il en serait différent avec un autre compilateur), alors que mon "concept" demande une fonction f.

    Par ailleurs, on peut voir qu'un concept qui ne se base pas uniquement sur des fonctions non membre demande un peu plus de tuyauterie pour être implémenté ainsi, et je ne fais pas confiance à mon compilateur pour, par exemple, supprimer la référence dans la classe de concept.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  6. #6
    Expert confirmé

    Inscrit en
    Novembre 2005
    Messages
    5 145
    Détails du profil
    Informations forums :
    Inscription : Novembre 2005
    Messages : 5 145
    Par défaut
    Les techniques pour faire de la vérification sur les paramètres templates sont bien connues. Certaines bibliothèques standards les utilisent d'ailleurs. Si l'introduction des concepts dans le langage offre un moyen de le faire, ce n'est par la seule utilité. Dans le même esprit, ils permettent aussi de vérifier que la définition ne fait usage que de l'interface demandée. Ils permettent enfin -- surtout? -- un certain nombre de choses en méta-programmation impossible à faire autrement ou bien au prix de l'utilisation de techniques plus ou moins obscures (la plus simple à expliquer est certainement la surcharge des templates sur des concepts).

  7. #7
    Membre confirmé
    Homme Profil pro
    Développeur .NET/C/C++
    Inscrit en
    Septembre 2007
    Messages
    71
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur .NET/C/C++
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Septembre 2007
    Messages : 71
    Par défaut
    Citation Envoyé par JolyLoic Voir le message
    Ca me semblait contraire aux règles du langage, j'ai donc fait un test pour vérifier...
    Oui, en fait mes tests concernaient le technique utilisant la clause using. Mes c'est vrai qu'en utilisant les surcharges, il faut au moins 1 appel explicite à la fonction pour que celle-ci soit instanciée.
    Par contre, en refaisant des tests j'ai pu constater que même en utilisant les using, il faut quand même instancier la classe au moins 1 fois en déclarant 1 variable par exemple.
    Au final et partant de ton exemple, on obtiens ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
     
    #include <iostream>
     
    struct A
    {
    	int g() {return 42;}
    };
     
    template<class T> 
    struct HasFAndG : public T
    {
    	HasFAndG() : T() {}
    	using T::f;
    	using T::g;
    };
     
     
    template<class T> int function(T &t)
    {
    	typedef HasFAndG<T> RealT;
    	RealT a; //cette ligne est nécessaire pour instancier la classe RealT
    	int result = t.g();
    	return result;
    }
     
    int main()
    {
    	A a;
    	std::cout << "Hello world!" << std::endl;
    	std::cout << function(a) << std::endl;
    	return 0;
    }
    Et à la comilation ça donne:

    tests_gcc.cpp: In instantiation of 'HasFAndG<A>':
    tests_gcc.cpp:21: instantiated from 'int function(T&) [with T = A]'
    tests_gcc.cpp:30: instantiated from here
    tests_gcc.cpp:13: erreur: no members matching 'A::f' in 'struct A'


    Maintenant, il est vrai aussi qu'en utilisant le using, on peut vérifier la présence d'une fonction, mais pas sa signature. Si on veut vérifier sa signature, alors il faut utiliser la surcharge plutot que le using
    (Ps: au passage, j'ai vu que tu encapsulait la classe à tester. Mais n'est-il pas plus simple d'en hériter?)
    Cela sous-entend qu'il faut alors instancier chaque fonction. Cela peut être fait en créant une fonction (éventuellement statique) dans la classe de test, laquelle va se charger de tout instancier et en y faisant un appel. Au final cela donne ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include <iostream>
     
    struct A
    {
    	int g() {return 42;}
    };
     
    template<class T> 
    struct HasFAndG : public T
    {
    	HasFAndG() : T() {}
    	int f() {return T::f();}
    	int g() {return T::g();}
    	static void Tester()
    	{
    		T a;
    		a.f();
    		a.g();
    	}
    };
     
    template<class T> int function(T &t)
    {
    	#ifdef _DEBUG_TEMPLATES_
    	typedef HasFAndG<T> RealT;
    	RealT::Tester();
    	#endif
    	int result = t.g();
    	return result;
    }
     
    int main()
    {
    	A a;
    	std::cout << "Hello world!" << std::endl;
    	std::cout << function(a) << std::endl;
    	return 0;
    }
    et quand on compile:
    tests_gcc.cpp: In static member function 'static void HasFAndG<T>::Tester() [with T = A]':
    tests_gcc.cpp:26: instantiated from 'int function(T&) [with T = A]'
    tests_gcc.cpp:38: instantiated from here
    tests_gcc.cpp:17: erreur: 'struct A' has no member named 'f'


    Effectivement, cette approche demande un peu plus de tuyauterie. Et puis j'ai aussi ajouter des #ifdef _DEBUG_TEMPLATES_ et #endif. En effet, je ne suis pas sur que le compilateur soi capable de détecter l'inutilité de l'appel à RealT::Tester();, aussi je préfères utiliser la précompilation pour éviter d'avoir des appels inutiles dans mon programme lorsque je le compile pour la production. Maintenant, il doit surement y avoir moyen de simplifier tout cela en utilisant des macros, mais bon. Le principe est là.
    Au final, je commence à comprendre l'intéret des concepts, et c'est vrai que même si il est possible d'arriver au même résultat autrement, les concepts permettent quand même de simplifier l'écriture de tout ceci.

Discussions similaires

  1. [Dexplore] Comment charger des fichiers d'aide ?
    Par Laurent Dardenne dans le forum Windows
    Réponses: 5
    Dernier message: 04/01/2005, 17h38
  2. [info - SQLpro] - nouvel article : probématique des NULL
    Par SQLpro dans le forum Décisions SGBD
    Réponses: 1
    Dernier message: 17/09/2004, 09h49

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo