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

Langage C++ Discussion :

Politique de range : begin et end, comment les implémenter, comment les appeler ?


Sujet :

Langage C++

  1. #1
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut Politique de range : begin et end, comment les implémenter, comment les appeler ?
    En C++11, il y a une pratique qui s'est faite avec les ranges. On peut relever sur le blog de Herb Sutter dans sa page de présentation du C++ moderne :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    sort(begin(v), end(v));
    Jusque là tout va bien, on nous explique que c'est générique, extensible, bref que c'est la crème.

    Seulement il y a un problème. Voyez-vous, ce code n'est générique que si v est un objet dont la classe est dans le namespace std. Dès lors que vous sortez des clous, c'est la misère.

    Si v n'est pas dans std:: et qu'il dispose de fonctions membres .begin() .end(), alors il sera nécessaire d'écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    sort(std::begin(v), std::end(v));
    Si vous avez défini des begin(decltype(v)&) et end(decltype(v)&) en fonctions libres, c'est le sort qui cette fois-ci ne va pas passer, il faudra alors écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::sort(begin(v), end(v));
    La seule solution qui vous permettra de garder ce code générique est de définir des std::begin(decltype(v)&) et std::end(decltype(v)&).

    Je ne sais qu'en penser, mais devoir ajouter des éléments dans le namespace std, ce n'est selon moi pas une preuve d'extensibilité, ni ce qu'on a l'habitude de faire.

    Que faire également si l'on souhaite spécialiser un algorithme ? Ou en ajouter un ? Doit-on également toucher au namespace std ?

    Quel sera l'avenir de la réponse apportée une fois qu'on se retrouve avec un design tel que sort(v); qui semble se profiler à l'horizon puisqu'on peut déjà voir Bjarne Stroustrup l'utiliser dans ses conférences ?

  2. #2
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Salut,

    Je crois que tu fais une erreur en essayant de prendre deux fonctionnalités bien précises pour en faire un mix dans ton raisonnement.

    Ce que je veux dire par là, c'est que le standard prévoit que la STL fournisse deux choses:
    1. un algorithme de tri qui se base sur un écart [begin, end[ d'une part et
    2. deux fonctions libres qui permettent de récupérer les bornes de l'écart en question d'autre part.
    Comme ces trois fonctionnalités sont imposées par la norme, il est normal qu'elles se trouvent dans l'espace de noms qui y est dédié, à savoir l'espace de noms std.

    A partir de là, il semble normal que tu doives "passer" par l'espace de noms std pour y accéder, si tu souhaites les utiliser.

    Rien ne t'interdit de définir tes propres fonctions libres begin et end pour des collections particulières, plutôt que d'envisager de créer une spécialisation des fonctions libres de la STL, et rien ne t'interdit non plus d'implémenter (et d'utiliser) ton propre algorithme de tri.

    A partir de là, rien ne t'interdit d'utiliser tes propres fonctions libres begin et end dans ton algorithme de tri personnel, à ce que je sache

    Mais, à partir du moment où tu feras le choix de ne pas fournir une version perso de ces trois possibilités et d'utiliser l'une (ou l'autre) de celles offertes par la STL, il semble tout à fait évident que tu devras passer par l'espace de noms std

    Et, quelque part, si tu préfères continuer à manipuler les fonctions membres begin et end de tes collections, j'ai envie de dire "grand bien te fasse"

    Le fait est que les fonctions libres apportent une plus grande flexibilité du simple fait qu'elles délèguent la responsabilité de l'accès à des éléments bien déterminés (et bien qu'elles soient souvent implémentées, pour les classes qui fournissent les fonctions membres, sous la forme de l'appel à ces fonctions membres )

    Mais elles ont, par contre, l'énorme avantage d'être, entre autres, spécialisées pour les tableaux "C style", sans qu'il ne soit nécessaire d'en connaitre la taille.

    Au final, je crois que ta question telle que posée ne présente que très peu d'intérêt (excuses moi cette franchise ), mais peut etre voulais tu surtout savoir quel intérêt il y aurait à utiliser les fonctions libres begin et end, quelle que soit l'utilisation qui en est faite derrière

  3. #3
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 34
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    Bonjour,

    Pour moi, la problématique de l'utilisation de begin/end est exactement la même que pour l'utilisation de swap :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    using std::begin;
    using std::end;
     
    ... begin(v) ...
    ... end(v) ...
    Pour l'implémentation, c'est à nouveau la même chose que pour swap :
    • Fonctions membres si possible.
    • Spécialisation si possible (à priori c'est rarement le cas pour les conteneurs, à moins que tu fasses des conteneurs non template, qui ne peuvent donc contenir qu'un type).
    • Fonctions begin/end libres dans le namespace de ta classe. (à condition de les utiliser comme on utilise std::swap, cf code précédent).

    A priori, si tu es l'auteur de la classe, tu peux proposer aussi bien la 1 et la 3. Si tu n'es pas l'auteur, se rabattre sur la 3 permet d'éviter de devoir modifier la classe (c'est à dire des modifications que tu devras refaire si l'auteur propose une nouvelle version de sa classe) en ouvrant seulement le namespace (chose que tu ne peux pas faire avec std, raison pour laquelle surcharger std::begin/std::end est exclu).

    A priori, contrairement à std::swap, on peut se passer de la 3 si on peut proposer la 1. Ici c'est une décision plus personnel, AMA.

    Pour l'appel d'un algorithme, il me semble normal de préciser le namespace devant l'appel. Et j'irais même jusqu'à faire
    Si je suis déjà dans un namespace. Les algo sont exprimés en termes d'abstractions pour justement ne pas dépendre des éléments abstraits. Pourquoi l'algorithme devrait être trouvé automatiquement ?

  4. #4
    Membre régulier
    Homme Profil pro
    Ingénieur
    Inscrit en
    Octobre 2006
    Messages
    48
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 37
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Ingénieur
    Secteur : Transports

    Informations forums :
    Inscription : Octobre 2006
    Messages : 48
    Points : 97
    Points
    97
    Par défaut
    C'est une question à la mode. Stackoverflow se pose la même question à l'instant :
    http://stackoverflow.com/questions/1...-end-functions

    Au final utiliser std::begin et std::end sans using declaration va à l'encontre de leur utilité.

  5. #5
    Membre expert

    Avatar de germinolegrand
    Homme Profil pro
    Développeur de jeux vidéo
    Inscrit en
    Octobre 2010
    Messages
    738
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Puy de Dôme (Auvergne)

    Informations professionnelles :
    Activité : Développeur de jeux vidéo
    Secteur : Tourisme - Loisirs

    Informations forums :
    Inscription : Octobre 2010
    Messages : 738
    Points : 3 892
    Points
    3 892
    Par défaut
    L'intérêt d'utiliser les fonctions libres du côté utilisateur je le vois totalement. Par contre du côté créateur c'est une belle plaie. Surtout quand on se retrouve avec un doublon de paires begin/end, dont l'une fait exactement ce que font les fonctions libres std:: (donc je me répugne fortement à le faire une deuxième fois en disciple du DNRY que je suis), à savoir appeler les fonctions membres begin/end (ou peut-être la méthode consacrée est-elle de faire des fonctions friend - bien qu'il me semble avoir plutôt lu prefer non-member non-friend ?).

    using std::begin;
    using std::end;
    2 lignes de plus à copier coller (hum hum) dans chaque fonction... dans le genre générique on a vu mieux...

    Va-t-on assister au grand retour du using namespace std ? Puisque toutes les vidéos de présentation du C++11 que j'ai pu voir montrent sans vergogne using namespace std; certes peut-être à des fins pédagogiques (pour ne pas effrayer tout le monde avec les std:: ou les using de partout ?), mais au final c'est ce que les gens retiennent : vous avez un code clean sans std:: et sans using de partout, pourquoi l'idée de se compliquer la vie leur viendrait-elle ? (De plus si même les grands noms du C++ ont honte de l'utilisation recommandée du C++ dans leurs présentations, c'est peut-être que la remettre en question n'est pas si inutile que ça ?).

    Pour l'appel d'un algorithme, il me semble normal de préciser le namespace devant l'appel.
    Parce que devoir taper std:: partout, à la limite sur les algos genre sort ça me dérange pas trop il a un nom trop flou qui lui donne un objectif non fixe, mais sur un find_if (qui est l'algo que j'utilise à chaque coin d'implémentation, bien plus que les for !), et bien plus encore sur un begin ou un end qui sont tout sauf flous dans leur sémantique (qui surtout me semble unique sauf si on me démontre le contraire).

    Mais elles ont, par contre, l'énorme avantage d'être, entre autres, spécialisées pour les tableaux "C style", sans qu'il ne soit nécessaire d'en connaitre la taille.
    C'est bien sympa de vendre la compatibilité à de futures utilisations de fonctionnalités deprecated, personnellement j'aimerais mieux la compatibilité avec des utilisations de fonctionnalités futures...

    Par exemple comment implémenteriez-vous un hypothétique find_if range-based dont le prototype générique pourrait être celui ci-dessous ? (ce n'est qu'une proposition, modifiable selon vos goûts) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    namespace std
    {
     
    template<class C, class UnaryPredicate>
    auto find_if(C& container, UnaryPredicate p);
     
    }

  6. #6
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    En fait, je crois que l'idée sous-jacente est surtout de se rendre compte que, la généricité aidant, il est possible de faire énormément de choses sous la forme de fonctions libres sans avoir besoin de copier 56 fois le code pour toutes les surcharges possibles donc à s'écarter de la mentalité "tout objet" dont on a pu constater les dérives, surtout lorsque l'on a affaire à des fonctionnalités "transversales".

    Je m'explique:

    En impératif pur (sans le générique et sans l'objet), en ne s'autorisant que la surcharge de fonction, tu devrais te taper un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Retour1 fonction (type1 /*const*/ & t){
        /* du code identique ici */
    }
    Retour2 fonction (type2 /*const*/ & t){
        /* que là */
    }
    /*...*/
    RetourN fonction (typeN /*const*/ & t){
       /* ou encore ici */
    }
    quand tu ne devrait pas, carrément, trouver des noms spécifiques à tes fonctions parce que la surcharge serait interdite.

    Le DRY en prend un sérieux coup pour la cause

    En orienté objet "pur", on aurait sans doute recours à l'héritage en créant une classe de base qui fournit l'interface, qui devra de toutes manières être implémentées dans toutes les dérivées.

    Le code serait tout autant copié, simplement, il serait plus "ventilé", et nous verrions un "god objet" pointer le bout de son nez très rapidement, peut etre d'avantage basé sur la donnée manipulée que sur le comportement, mais quand même

    L'approche "générico-OO" (comprend, vu que le terme n'existe pas, l'approche consistant à utiliser des fonctions membres dans des classes génériques) permet de rétablir un peu l'équilibre en se basant sur le comportement des fonctions membres, indépendamment du type de la donnée manipulée.

    Enfin, l'approche des fonctions libres génériques te permet de définir un comportement tout à fait transversal sans avoir à t'inquiéter d'une manière ou d'une autre de savoir si la donnée que tu vas manipuler dispose de l'interface requise.

    A partir du moment où tu sais que std::begin va te donner l'accès au premier élément d'une collection quelconque et que end va symboliser ce qui suit le dernier élément de cette même collection, quel que soit le moyen mis en oeuvre pour fournir ce résultat tu peux envisager d'écrire un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T>
    void foo(){
        T collection = fonctionQuiRenvoieUneCollectionQuelconque();
        auto first = std::begin(collection);
        auto afterLast = std::end(collection);
        while(first!=afterLast){
            doSomething();
        }
    }
    et ce, même si la collection en question présente une interface que tu n'avais pas prévue au moment d'écrire le code.

    Avec les collections de la STL, le fonctionnement offre déjà la garantie de fonctionnement, ainsi qu'avec des tableaux C style (qui peuvent parfaitement avoir une taille fixe arbitraire )

    Mais, si tu décides un jour de créer une collection dont c'est la fonction membre truc qui renvoie le premier élément et la fonction bidule qui symbolise ce qui suit le dernier élément, tu n'auras qu'à fournir une spécialisation partielle de std::begin et std::end (ce qui sera assez facile en somme) pour que tout le code susceptible de les utiliser devienne, comme par magie, compatible avec ta collection bizarre.

    Et si un jour, tu veux faire passer ta collection dans un algorithme qui n'était pas sensé l'utiliser ou pour lequel la collection n'était pas prévue à la base, la spécialisation de std::begin et de std::end sera quand meme sans doute beaucoup plus facile à mettre en oeuvre que tout ce que tu pourrais devoir faire pour adapter ton algorithme ou en implémenter une version "compatible".

    J'ai presque envie de rappeler que tout code ne vaut que par l'utilisation qui en est faite.

    Tu peux avoir un algorithme simplement génial sensé pouvoir s'appliquer dans de nombreuses situations, si tu n'est pas en mesure de fournir une implémentation qui soit suffisamment flexible pour ne pas devoir l'adapter à chaque nouvelle situation qui se présente, ton code perd finalement beaucoup de son intérêt.

    Les fonctions std::begin et std::end devront sans doute être adaptées à certaines situations loufoques, mais elles ouvrent malgré tout la voie à une flexibilité accrue par l'utilisation qui en sera faite

  7. #7
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 34
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    @koala: Specilisation partielle de fonction template c'est pas standard. Bien pour ca que l'usage est de faire un using et un appel non qualifie pour profiter de l'adl.

    @OP: begin/end c'est lourd par nature, c'est (les iterateur) une abstraction qui vaut ce qu'elle vaut, l'abstraction supplémentaire c'est le concept de range. Tu ne veux pas de la lourdeur alors utilise ce concept (via boost::range).

    Ta comparaison entre begin et sort est non avenue ama , sort est un algorithme, begin non.

Discussions similaires

  1. Réponses: 2
    Dernier message: 22/08/2007, 12h46
  2. Réponses: 4
    Dernier message: 14/05/2007, 22h24
  3. Réponses: 7
    Dernier message: 24/01/2007, 16h05
  4. Réponses: 6
    Dernier message: 16/01/2007, 21h34

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