Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

Going Native 2013 - Stephan Lavavej - Don't help the compiler

Le , par Linunix, Membre confirmé
Stephan Lavavej - Don't help the compiler
GoingNative 2013

La conférence de Stephan. T. Lavavej (alias S.T.L.) lors des GoingNative 2013 est maintenant disponible en rediffusion :


Voici un résumé de cette conférence :

Les lambdas ne sont pas toujours mieux

Utilisez certains foncteurs plutôt que des lambdas.

Que pensez-vous de ce code ?
Code : Sélectionner tout
sort(v.begin(), v.end(), [] (const Elem& l, const Elem& r) { return l > v; }
Eh bien, Lavavej explique qu'il est verbeux. En effet, il préconise l'utilisation du foncteur greater dans ce cas là, de la manière suivante :
Code : Sélectionner tout
sort(v.begin(), v.end(), std::greater<Elem>());
Les foncteurs greaters, plus, multiplies :
Code : Sélectionner tout
1
2
3
4
template<class T> 
struct plus { 
T operator()(const T& x, const T& y) 
{ return x + y; }
Code : Sélectionner tout
1
2
3
4
5
template<class T> 
struct multiplies { 
T operator()(const T& x, const T& y) 
{ return x * y; } 
};
Code : Sélectionner tout
1
2
3
4
5
template<class T> 
struct greater { 
bool operator()(const T& x, const T& y) 
{ return x > y; } 
};
À travers ces codes S.T.L. veut nous montrer la simplicité d'utilisation de ces foncteurs par rapport aux lambdas qui sont plus verbeuses. Utiliser certains foncteurs intéressants et utiles (greater, less, plus, multiplies, et d'autres...) permet de gagner grandement en lisibilité.

Les diamonds

Au cours de cette conférence, Stephan T. Lavavej a également présenté, un nouveau type de foncteurs, nommés "diamonds" sur lesquels il travaille.

Il a effectué une proposition pour la standardisation du C++14. Voici un code d'exemple d'utilisation :

Code : Sélectionner tout
1
2
3
4
vector<const char *> v { "cute", "fluffy", "kittens" }; 
set<string, greater<>> s { "Hungry", "EVIL", "ZOMBIES" }; 
vector<string> dest; 
transform(v.begin(), v.end(), s.begin(), back_inserter(dest), plus<>());
Grâce aux codes suivants, avec les conteneurs vector et set, ainsi que l'algorithme transform, Lavavej explique, et prouve leur simplicité d'utilisation, ainsi que leur côté extrêmement pratique.

En résumé :
Utilisez les foncteurs "diamonds" lorsqu'ils seront disponibles, cela améliorera la lisibilité.
En plus d'être moins verbeux, ils sont plus efficaces et permettent d'éviter les divers dangers de conversion de types.

L'utilité du const X&&

Dans cette petite partie, S.T.L. nous explique que :

  • const X&& n'est pas utile pour la sémantique de déplacement ;
  • const X&& n'est pas parfait pour le transfert parfait également ;
    Excepté X&& qui déclenche la déduction d'argument d'un template.


Cependant, const X&& permet de rejeter des temporaires.

Synthèse générale :
  • Apprenez à utiliser les références rvalues.
  • Ne surchargez pas les transferts parfaits, car ils sont intentionnellement gourmands.
  • Utilisez les temporaires seulement quand ils sont nécessaires.
  • Utilisez "const X&&" pour rejeter les temporaires
  • Mais surtout essayez d'écrire du code uniquement lorsque vous savez comment celui-ci va se comporter. Cela améliore votre conception et réduit le nombre de modifications apportées à votre code par rapport à un développement sans réflexion.

    C'est généralement vrai, mais spécialement pour les références rvalues, sinon veuillez demander, ou vous faire conseiller par un expert.
  • Utilisez certains foncteurs (greater, plus, multiplies, less, etc...), plutôt que les lambdas qui nuisent à la lisibilité.



Évènement : GoingNative 2013

Et vous
Qu'en pensez-vous ?


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster une réponse

Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 14/09/2013 à 22:06
Salut,

Ca me rassure, car je me souviens de quelques débats dans lesquelles je prenais des position similaires.

Je ne nies absolument pas l'intérêt des expressions lambda, mais je crois que, comme toute nouvelle technique, il est important de se l'approprier et de trouver les cas où elle apporte réellement un plus par rapport à l'existant.

Le gros intérêt des foncteurs, selon moi (quel que soit le nom qu'on puisse leur donner, du moment qu'ils soient explicites ) est qu'il est possible de les réutiliser bien plus qu'une expression lambda et qu'il y a même moyen de les généraliser si l'on est un peu attentif aux différents concepts mis en oeuvre dans un projet
Avatar de germinolegrand germinolegrand - Membre expert https://www.developpez.com
le 14/09/2013 à 22:36
J'avoue en effet être forcé de réécrire assez souvent le même genre de lambdas. La plupart du temps, c'est parce que je dois déréférencer un pointeur dans la comparaison. Une façon d'améliorer ça serait utile.
Avatar de Linunix Linunix - Membre confirmé https://www.developpez.com
le 14/09/2013 à 23:02
Citation Envoyé par koala01
Je ne nies absolument pas l'intérêt des expressions lambda, mais je crois que, comme toute nouvelle technique, il est important de se l'approprier et de trouver les cas où elle apporte réellement un plus par rapport à l'existant.

Tout à fait d'accord.

Comme, je disais tout à l'heure a germinolegrand, la vision de lavavej sur les lambdas est je pense une vision que bien du monde devrait avoir.
A savoir certes elles sont tres pratiques mais dans certains cas, on a des foncteurs qui peuvent faire l'affaire.

Maintenant le fait est qu'au niveau d'une reutilisation rien ne vaut les foncteurs.

Cependant je trouve qu'une lambda utilisée avec auto ameliore la lisiblité d'un code.

C'est d'ailleurs ce que j'essaye de faire en ce moment c'est à dire :
Favoriser une utilisation des foncteurs, plutot qu'une lambda, dans certains cas comme l' exemple de S.T.L.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 14/09/2013 à 23:25
Citation Envoyé par germinolegrand  Voir le message
J'avoue en effet être forcé de réécrire assez souvent le même genre de lambdas. La plupart du temps, c'est parce que je dois déréférencer un pointeur dans la comparaison. Une façon d'améliorer ça serait utile.

En fait, on en revient toujours au fait qu'il faut avoir une vue "transversale" de son projet : il y a des "concepts" qui apparaissent, de manière générale, dans presque la totalité de tes hiérarchies de classes.

Mettons que tu travailles sur des objets pour lesquels tu aies décidé de fournir à chaque fois un identifiant unique.

Tu auras sans doute travaillé correctement en veillant à garder tes hiérarchies clairement distinctes, mais la plupart d'entre elle auront une propriété commune : une fonction id() (ou getId() ou ce que tu veux) qui renvoie l'identifiant unique de l'objet en question

Le fait est que cet identifiant unique est un très bon candidat pour les tentatives de classement, et que tu vas donc sans doute régulièrement utiliser une lambda pour vérifier quel identifiant est plus petit que l'autre (dans une même game d'objets, s'entend)

Dés lors, pourquoi ne pas créer un foncteur spécial sous la forme de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
template <typename T> 
struct lessById{ 
    bool operator()(T const & first, T const & second) const{ 
        return first.id() < second.id(); 
    } 
    /* comme cela, on prend les pointeurs en compte également ;) */ 
    bool operator()(T * first, T * second) const{ 
        return this->(*first, *second); 
    } 
};
Tu l'écris une fois, tu l'utilises partout où tu en as besoin

Puis, tu en viens à te rendre compte que pas mal de tes hiérarchies présente le concept de "positionnable" et que tu tries souvent des objets en fonction de la position à laquelle ils se trouvent.

Hé bien, c'est reparti : on crée un autre foncteur qui pourrait être proche de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
template <typename T> 
struct lessByPosition{ 
    bool operator()(T const & first, T const & second) const{ 
        return first.position().x() < second.position().x() || 
                 ( first.position.x() == second.position.x() && 
                   first.position.y() < second.position.y() ); 
    } 
    /* comme cela, on prend les pointeurs en compte également ;) */ 
    bool operator()(T * first, T * second) const{ 
        return this->(*first, *second); 
    } 
};
ET ainsi de suite pour chaque concept plus ou moins transversal

Maintenant, il n'y a strictement rien qui ne t'empêche de faire pareil pour un concept qui ne serait utilisé que pour un certain type bien particulier pour lequel les comparaisons sont fréquentes:
Code : Sélectionner tout
1
2
3
4
5
6
7
8
void lessBySomething{ 
    bool operator()(SomeType const & first, SomeType const & second) const{ 
        /* the correct logic to use */ 
} 
    bool operator()(SomeType  * first, SomeType  * second) const{ 
        return this->(*first, *second); 
    } 
};
Ce qui importe, c'est qu'au lieu de copier sans cesse tes labmda (qui sont effectivement particulièrement verbeuses), tu te retrouve avec une seule "version" du code, qui se trouve à un endroit bien déterminé dans ton projet, et que, si tu dois en corriger la logique, tu n'auras qu'un seul endroit du code à vérifier.

Les expressions lambda devraient, au final, être réservées à des situations dont on a la certitude qu'elles ne serviront qu'à cet endroit bien particulier
Avatar de Caramorgar Caramorgar - Inactif https://www.developpez.com
le 15/09/2013 à 10:17
Au final, c'est juste un rappel des règles existantes (factoriser son code, utiliser l'existant), rien de nouveau. C'est juste que les gens oublient souvent ces règles de base (@germinolegrand non non, je vise personne... tu en es où avec l'écriture de ta lib de GUI ? )
Avatar de germinolegrand germinolegrand - Membre expert https://www.developpez.com
le 15/09/2013 à 19:15
Fort heureusement mes lambdas sont toujours assez courtes et simples pour qu'il soit plus rapide/facile de les écrire sans aller voir ailleurs ^^ (vite écrit quand on a le coup de main ).

Ce sont les foncteurs diamonds qui sont intéressants parce qu'ils nous offrent dores et déjà des foncteurs polymorphiques .

Et justement, ils risquent de m'aider pour la GUI sur laquelle je travaille actuellement (qui n'a rien à voir avec la GUI entièrement basée sur les std::function que j'ai laissée tomber bien que résolvant tous les problèmes de mise à jour de valeurs dynamiques). Je me retrouve avec des macros parce que je ne pouvais pas passer des fonction template en tant que foncteur... ce que vise à résoudre cette technique pour laquelle je remercie S.T.L. d'en avoir montré les ficelles .
Avatar de Linunix Linunix - Membre confirmé https://www.developpez.com
le 15/09/2013 à 19:32
Citation Envoyé par germinolegrand  Voir le message
Fort heureusement mes lambdas sont toujours assez courtes et simples pour qu'il soit plus rapide/facile de les écrire sans aller voir ailleurs ^^ (vite écrit quand on a le coup de main ).

Ce sont les foncteurs diamonds qui sont intéressants parce qu'ils nous offrent dores et déjà des foncteurs polymorphiques .

Pour la question, qu'on se posait dernierement, j'ai bien la confirmation les diamonds ont été voté et seront donc implémentés, on a donc dépassé le stade de la proposition pour C++14

D'ailleurs elles seraient dans le pdf N3690 du draft concernant les features du C++14.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 15/09/2013 à 20:24
Citation Envoyé par germinolegrand  Voir le message
Fort heureusement mes lambdas sont toujours assez courtes et simples pour qu'il soit plus rapide/facile de les écrire sans aller voir ailleurs ^^ (vite écrit quand on a le coup de main ).

Peu importe leur taille, le gros reproche qu'on peut leur faire (non, le terme est trop fort, mettons : le gros point contre lequel il faut mettre le développeur en garde) est, justement, la raison pour laquelle elles ont été créées : le fait que le scope dans lequel la lambda est accessible est excessivement localisé.

Je m'explique:

Imaginons que tu aies une classe A qui prenne la forme de
Code : Sélectionner tout
1
2
3
4
class A{ 
    public: 
    long long id() const; 
};
Tu as une classe spécifique qui contient une collection de A, sans doute proche de
Code : Sélectionner tout
1
2
3
4
5
class AHolder{ 
 
private: 
    std::vector<A> tab; // ou n'importe quel autre type de collection ;) 
};
Jusque là, il n'y a pas de problème

Puis tu rajoutes une fonction à ta classe AHolder sous une forme proche de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
class AHolder{ 
public: 
   /* ... */ 
     void foo(){ 
        std::sort(tab.begin(),tab.end(), 
                  [](const A & a, const A & b) -> bool{return a.id() <b.id();}); 
        /* ... */ 
    } 
private: 
    std::vector<A>; // ou n'importe quel autre type de collection non triée;) 
};
Jusque là, je n'ai toujours aucun problème.

Mais ca va commencer à se corser avec l'ajout de bar, qui nécessite aussi de trier la collection...

On risque en effet, si le développeur est distrait (ou qu'il n'y pense pas ou que... va savoir quoi) de se retrouver avec quelque chose comme
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AHolder{ 
public: 
   /* ... */ 
     void foo(){ 
        std::sort(tab.begin(),tab.end(), 
                  [](const A & a, const A & b) -> bool{return a.id() <b.id();}); 
        /* ... */ 
    } 
     void bar(){ 
        std::sort(tab.begin(),tab.end(), 
                  [](const A & a, const A & b) -> bool{return a.id() <b.id();}); 
        /* ... */ 
        } 
    } 
private: 
    std::vector<A>; 
};
Avec un peu de chance, les deux lambda seront assez proche dans le code pour que le développeur se rende compte qu'il a écrit strictement deux fois la même expression, et pour qu'il factorise un peu cela sous la forme de
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AHolder{ 
public: 
   /* ... */ 
     void foo(){ 
         sortById(); 
         /* ... */ 
    } 
     void bar(){ 
         sortById(); 
         /* ... */ 
    } 
private: 
    void sortById(){ 
     
        std::sort(tab.begin(), tab.end(),  
            [](A const & a, A const & b)->bool{ return a.id() <b.id(); } ); 
    } 
    std::vector<A>; 
};
Ce qui aurait pour effet de rendre l'utilisation raisonnable, car elle évite la duplication de code.

Mais voilà qu'apparait une nouvelle classe : B, qui n'a strictement rien à voir avec A, à part que, elle aussi, elle expose une fonction id() sous la forme de
Code : Sélectionner tout
1
2
3
4
class B{ 
    public: 
    long long id() const; 
};
Et bien sur, on a un holder qui va avec, sous la forme de
Code : Sélectionner tout
1
2
3
4
5
class BHolder{ 
   /* ... */ 
private: 
    std::vector<B> tab; // ou n'importe quel autre type de collection ;) 
};
Les deux domaines sont tellement séparés qu'il n'y a aucune chance pour que A et B (ou pour que AHolder et BHolder) soient utilisés ensemble.

Mais, en cours de développement, tout ce que j'ai fait subir à AHolder, je peux le faire subir à BHolder. Comprends par là que je peux y ajouter deux fonctions qui, dans une première version, utiliseraient chacune sa propre expression lambda et que je peux décider de la factoriser (en donnant d'ailleurs sans doute le même nom de sortById).

C'est très bien, si l'on regarde AHolder et BHolder pris séparément.

Mais ca commence vraiment à me poser un problème si l'on envisage le projet dans son ensemble : nous nous retrouvons en effet avec du code dupliqué et strictement identique, et ca, ca devient dangereux parce que, s'il advient que tu as fait une erreur de logique, il y a de fortes chances pour que tu l'aies reproduite aux deux endroits.

Et, dans ce cas, tu finiras tôt ou tard te rendre compte que sortById ne donne pas le résultat escompté, peut être pour AHolder ou peut être pour BHolder, et tu apporteras la correction nécessaire... Pour la classe pour laquelle tu as remarqué l'erreur, en oubliant (très vraisemblablement) qu'il faut corriger cette erreur à deux endroits.

Bien sûr, on pourra me dire que ce n'est pas avec un simple test comme a.id() < b.id() que l'on risque de se tromper, mais qu'en est-il des expressions lambda "à peine plus compliquées"

Ecrire une expression lambda lorsque l'on a la certitude qu'il ne faudra pas la dupliquer n'est pas une mauvaise chose en soi. Mais peut-on vraiment avoir cette certitude

Tu me répondras sans doute que rien ne t'empêche d'écrire une première version qui utilise une expression lambda et de la transformer en foncteur (c'est si simple ) si tu te rend compte que tu commence à la dupliquer, et je t'accorderai ce point, à un bémol près : es-tu sur que tu te souviendras d'avoir déjà écrit la même expression dans une semaine ou dans six mois
Avatar de germinolegrand germinolegrand - Membre expert https://www.developpez.com
le 15/09/2013 à 22:53
Je vais être audacieux, et répondre autre chose :
Si les codes identiques ne sont pas suffisamment proches pour que je les remarque, alors il n'y a pas de raison de factoriser. Oui, à trop vouloir factoriser, on perd son temps (sans parler de factoriser d'avance, mais tu connais mon opinion là dessus ^^), c'est comme l'over-généricité. En effet, si comme tu le dis ton A et ton B n'ont aucune raison d'être manipulées dans un même morceau du programme, faire une factorisation entre les deux ne rime à rien : peut-être voudrai-je modifier A plus tard, je ne voudrais pas que cela affecte B, hop je me retrouve avec un niveau d'abstraction inutile pour B.

Parfois un peu de duplication n'est pas mauvaise. Tant qu'elle n'est pas faite sciemment, et que les codes sont indépendants. Pour ce qui est des bouts de code à utilité générale, ça va dans votre extension STL perso, et ça ça mérite d'être factorisé à l'échelle du projet.
Avatar de koala01 koala01 - Expert éminent sénior https://www.developpez.com
le 16/09/2013 à 0:33
Citation Envoyé par germinolegrand  Voir le message
Je vais être audacieux, et répondre autre chose :
Si les codes identiques ne sont pas suffisamment proches pour que je les remarque, alors il n'y a pas de raison de factoriser. Oui, à trop vouloir factoriser, on perd son temps (sans parler de factoriser d'avance, mais tu connais mon opinion là dessus ^^), c'est comme l'over-généricité. En effet, si comme tu le dis ton A et ton B n'ont aucune raison d'être manipulées dans un même morceau du programme, faire une factorisation entre les deux ne rime à rien : peut-être voudrai-je modifier A plus tard, je ne voudrais pas que cela affecte B, hop je me retrouve avec un niveau d'abstraction inutile pour B.

Justement, c'est là que je ne suis pas d'accord : une abstraction n'est jamais inutile.

Elle peut sembler l'être à un moment donné, mais, dis toi que si tu as utilisé une abstraction pour comparer tes id parce qu'une classe en avait besoin c'est que tu peux très bien en avoir besoin ailleurs.

Tu veux modifier tes A pour qu'ils soient triés en fonction de leur nom Rien de plus facile : tu crées un autre foncteur (lessByName) et tu l'utilises uniquement là où tu en as besoin.

On peut encore discuter sur l'intérêt de le faire pour deux classes, mais si au final c'est dix, quinze ou vingt classes qui sont manipulées, qui doivent être triées parfois en fonction d'un nom, parfois en fonction d'un id ou que sais-je, le fait d'avoir une abstraction qui te permet de le fait t'autorise, justement, à jongler avec ces abstractions bien plus facilement que ton expression lambda.

Je ne nie absolument pas l'intérêt des expressions lambda, je dis juste qu'elle doivent, comme toute technique avancée de programmation, être utilisées à bon escient.

Et il n'y a rien à faire, à mon sens, le "bon escient" se trouve être en l'occurrence un comportement à ce point spécialisé qu'il n'y a strictement aucune chance pour que l'on en vienne à dupliquer le code d'une manière ou de l'autre

Maintenant, c'est mon avis strictement personnel (qui semble malgré tout partagé par S.T.L ) et je ne t'obliges absolument pas à être d'accord
Offres d'emploi IT
Ingénieur analyste programmeur (H/F)
Safran - Auvergne - Montluçon (03100)
Architecte et intégrateur scade/simulink H/F
Safran - Ile de France - Vélizy-Villacoublay (78140)
Responsable transverse - engagement métiers H/F
Safran - Ile de France - Corbeil-Essonnes (91100)

Voir plus d'offres Voir la carte des offres IT
Contacter le responsable de la rubrique C++