Developpez.com - Rubrique C++

Le Club des Développeurs et IT Pro

C++ : auto ou const auto pour les lambdas ? Pourquoi ?

Par Bktero

Le 2024-02-14 16:01:18, par Bktero, Modérateur
Bonjour

Je suis de la team "const autant que possible". Donc, dès que je vois une déclaration de variable à laquelle je peux ajouter const, je le fais.

Je fais une exception à cette règle : quand je stocke une lambda expression, je n'ajoute jamais const. Je fais auto foo = []() {}; et non const auto foo = []() {};.

Pourtant, en suivant la logique "const autant que possible", je devrais aussi utiliser const dans cette situation.

Comment faites-vous ? auto ou const auto pour les lambdas ? Pourquoi ?

PS : pour celles et ceux qui ne savent pas, on ne peut changer la valeur de la variable :
Code :
1
2
3
4
int main() {
    auto f = []() {};
    f = []() {};
}
Code :
1
2
3
4
5
6
7
8
<source>:3:7: error: no viable overloaded '='
    3 |     f = []() {};
      |     ~ ^ ~~~~~~~
<source>:2:14: note: candidate function (the implicit copy assignment operator) not viable: no known conversion from '(lambda at <source>:3:9)' to 'const (lambda at <source>:2:14)' for 1st argument
    2 |     auto f = []() {};
      |              ^
1 error generated.
Compiler returned: 1
EDIT : on peut produire un exemple où le typage correspond et où l'affectation est quand même rejetée (de toute façon, c'est pas un cas très utile) :

Code :
1
2
3
4
5
int main() {
    auto f = []() {};
    auto g = f;
    f = g;
}
Code :
1
2
3
4
5
6
<source>:4:6: error: object of type '(lambda at <source>:2:14)' cannot be assigned because its copy assignment operator is implicitly deleted
    4 |         f = g;
      |           ^
<source>:2:14: note: lambda expression begins here
    2 |     auto f = []() {};
      |              ^
  Discussion forum
19 commentaires
  • ternel
    Expert éminent sénior
    Envoyé par Bktero
    J'ai l'impression que personne n'a de bons arguments pour ne pas mettre const
    Ce qui est en soit une bonne raison pour le mettre.

    Je préfère être explicite, et je cite les C++ Core Guidelines:
    I.1: Make interfaces explicit
    ES.25: Declare an object const or constexpr unless you want to modify its value later on
  • ternel
    Expert éminent sénior
    A priori je suis d'accord qu'il faudrait un const.

    Cela dit, j'ai une réserve sur ton exemple.
    Chaque lambda est d'un type unique. Du coup, dans ton exemple, tu essaies d'affecter deux types non compatibles.

    Par contre, j'ai passé dix minutes à trouver un contre exemple, et j'arrive quand même à un refus.
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    int main() {
        int state = 0;
        auto f = [state]() mutable { return ++state; };
        auto backup = f;
        auto g = f;
        for (int i = 0; i < 10; ++i) std::cout << f() << ' ';
        for (int i = 0; i < 10; ++i) std::cout << g() << ' ';
        f = backup;
        return 0;
    }
    Avec GCC, en C++17, ce code-ci échoue avec le message suivant, mais uniquement à la ligne f = backup;.
    error: use of deleted function ‘main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&
  • koala01
    Expert éminent sénior
    Salut,
    Envoyé par ternel
    Ce qui est en soit une bonne raison pour le mettre.

    Je préfère être explicite, et je cite les C++ Core Guidelines:
    I.1: Make interfaces explicit
    ES.25: Declare an object const or constexpr unless you want to modify its value later on
    Et tu as tout à fait raison avec ce point. Il faut juste se mettre d'accord sur les termes que l'on utilise...

    Car la ligne directrice ES25 parle d'objets, et non d'expressions. C'est donc qu'il est utile de faire la distinction entre les expressions qui représentent effectivement objets -- comme les données et les constantes -- et les expressions qui ne représentent pas des objets, comme les fonctions.

    Or, les expressions lambda sont plus proche des fonctions que des objets, ce qui fait qu'il n'y a finalement que peut de raison de les déclarer constantes.

    Tu pourras, bien sur, me dire que les fonction peuvent renvoyer une valeur constante, et que l'on peut même trouver facilement une situation dans laquelle c'est effectivement la fonction qui est déclarée constante (lorsque l'on a affaire à une fonction membre).

    Dans le premier cas, cela ne change rien, vu que toutes les fonctions ne renvoient pas forcément une donnée, et que la constance de la donnée renvoyée n'a -- en définitive -- absolument rien à voir avec le comportement de la fonction en elle-même.

    Dans le deuxième cas (les fonctions membres constantes), la constance n'a pour but que d'indiquer le fait que la donnée à partir de laquel la fonction sera appelée ne sera pas modifiée. Ce qui nous place bel et bien sur une constance d'objet...

    Lorsque tu assignes une expression lambda à f (ou à g, ou à tout ce que tu peux vouloir) sous la forme de f =[](){}, tu crées d'avantage un alias sur une expression (f étant au final une expression) qu'un objet (une donnée dont le type serait l'expression elle-même), le mot clé auto étant là pour représenter le retour de l'expression dans le cas où l'expression serait destinée à renvoyer une valeur.

    Au final, nous pourrions dire que "oui, il faut rendre tous les objets que l'on peut constants, à moins d'avoir de bonnes raisons de croire qu'ils devront être modifiés", dans le respect de la ES25; cependant, il faut se rendre compte que seules les données sont susceptibles d'être considérées comme des objets
  • ternel
    Expert éminent sénior
    A vrai dire, je considère ce cas completement théorique: je ne mets jamais une lambda dans une variable locale.

    Et s'il y a bien une chose que je trouve dommage en C++, c'est qu'il faille préciser const plutot que modifiable. Aujourd'hui, je préfèrerai qu'un nom soit une constante par défaut.

    Bref, j'ai vaguement un argument de cohérence. Comme j'essaie de toujours déclarer mes variables locale auto const, je ferai de même pour une lambda.
  • Luc Hermitte
    Expert éminent sénior
    const, ce n'est pas tant une histoire de prévenir les bugs sur les variables locales -- si on excepte le cas des paramètres sortants qui sont pris par référence et pas par pointeur (j'ai horreur par pointeur). (Pour ceux qui ont besoin d'explicite, je préfère de loin un décorateur de type genre `out<>` à constructeur explicite)

    Pour moi, const sur les variables locales, c'est une aide à l'analyse de code des jours/semaines/mois/années après. Que cela soit pour comprendre ce que fait un algo que pour débugguer (ce qui revient un peu au même) => on sait alors que quand on déroule pas à pas (/à la main) un code pour comprendre ce qu'il fait/ce qui cloche, et bien il y a des données dont on n'a plus besoin de surveiller l'évolution. Et ça c'est cool.

    C'est juste pas compatible avec le NRVO
  • Astraya
    Membre chevronné
    Hello,
    Cela fait longtemps que je ne suis pas venu ici !
    Ça n'a pas trop de sens d'avoir un lambda const car par defaut l'operator() est const.

    Une lambda ce n'est rien de plus que du sucre syntaxique d'un foncteur, donc si f est const il ne peux modifier son état dans l'operator(), donc ne peut modifier ces variables internes comme toutes méthodes const d'une classe, or l'operator() étant const par défaut, this est const par défaut dans l'operator().

    Cela n'apporte rien.
  • Bktero
    Modérateur
    Mon seul bon argument pour ne pas ajouter const est effectivement que ça ne sert à rien, la variable étant en quelque sorte "implicitement const". L'autre argument que je peux avoir c'est "ça met plus en évidence la lambda", mais il me semble un peu léger. J'ai l'impression que personne n'a de bons arguments pour ne pas mettre const

    Les raisons techniques du langage derrière ne sont pas vraiment intéressantes dans un document de "code style", et ce n'est d'ailleurs pas le sujet ici
  • mintho carmo
    Membre éclairé
    Envoyé par koala01 
    Or, les expressions lambda sont plus proche des fonctions que des objets, ce qui fait qu'il n'y a finalement que peut de raison de les déclarer constantes.

    Mouais. C'est quand même tiré par les cheveux de faire une telle distinction. cppreference parle même de "unnamed function object" pour désigner une lambda. A mon sens, l'argument de ternel est complètement vailde.

    Envoyé par Astraya 
    Une lambda ce n'est rien de plus que du sucre syntaxique d'un foncteur, donc si f est const il ne peux modifier son état dans l'operator(), donc ne peut modifier ces variables internes comme toutes méthodes const d'une classe, or l'operator() étant const par défaut, this est const par défaut dans l'operator().

    Pour "modifier ces variables internes" (c'est à dire les captures), il faut de toute façon explicitement mettre "mutable".

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream> 
     
    int main() { 
        int i = 123; 
        const auto f = [i]() { 
            std::cout << i << std::endl; // ok, on modifie pas la capture 
        }; 
        f(); 
    }
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream> 
     
    int main() { 
        int i = 123; 
        const auto f = [i]() { 
            i = 321; // error: cannot assign to a variable captured by copy in a non-mutable lambda 
        }; 
        f(); 
    }
    Il faut donc écrire "auto f = [i]() mutable {" dans ce cas. Un truc qui est a peut prêt sur, c'est que mettre const et mutable ensemble, ça n'aurait pas de sens. (Et je suis pas sur du tout que ce soit possible, de toute façon).

    Donc sans mutable, cela veut dire que la lambda ne modifie pas son état interne. C'est ce que disait Bktero :

    Envoyé par Bktero 
    la variable étant en quelque sorte "implicitement const"

    Et donc on pourrait tout a fait avoir la règle : soit const, soit mutable.

    Envoyé par Bktero 
    J'ai l'impression que personne n'a de bons arguments pour ne pas mettre const

    Pour être honnête, je pense que le seul argument, c'est juste "par habitude". Il faudrait voir différents projets, pour voir ce qui se fait, mais j'ai l'impression que c'est juste "on fait pas comme ça", sans raison technique spécifique.
  • koala01
    Expert éminent sénior
    Envoyé par mintho carmo 
    Mouais. C'est quand même tiré par les cheveux de faire une telle distinction. cppreference parle même de "unnamed function object" pour désigner une lambda. A mon sens, l'argument de ternel est complètement vailde.

    C'est tout aussi tiré par les cheveux de penser que n'importe quelle expression peut être considérée comme un objet pouvant être constant ou non

    Pour "modifier ces variables internes" (c'est à dire les captures), il faut de toute façon explicitement mettre "mutable".

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <iostream> 
     
    int main() { 
        int i = 123; 
        const auto f = [i]() { 
            i = 321; // error: cannot assign to a variable captured by copy in a non-mutable lambda 
        }; 
        f(); 
    }
    Sauf que tu auras exactement le même résultat avec f déclarée sous la forme de auto f = [i](){ i = 456;}; ... La preuve ==>ICI<==...

    Le tout, sans même mentionner les règles d'application du mot clé const, qui, par défaut, s'applique à ce qui se trouve à sa gauche, sauf si rien ne le précède, car dans ce cas, il s'applique à ce qui le suit (se trouve à sa droite).

    Le mot clé const s'applique donc dans le cas présent à auto, qui correspond à ce que l'expression lambda est sensée renvoyer (si, du moins, elle renvoie quelque chose), et non à f (qui est l'alias sur l'expression)

    Il faut donc écrire "auto f = [i]() mutable {" dans ce cas. Un truc qui est a peut prêt sur, c'est que mettre const et mutable ensemble, ça n'aurait pas de sens. (Et je suis pas sur du tout que ce soit possible, de toute façon).

    Donc sans mutable, cela veut dire que la lambda ne modifie pas son état interne. C'est ce que disait Bktero :

    Et donc on pourrait tout a fait avoir la règle : soit const, soit mutable.

    Pour être honnête, je pense que le seul argument, c'est juste "par habitude". Il faudrait voir différents projets, pour voir ce qui se fait, mais j'ai l'impression que c'est juste "on fait pas comme ça", sans raison technique spécifique.[/QUOTE]
  • mintho carmo
    Membre éclairé
    TL;DR. Ajouter un nouveau concept "comportement" (qui n'existe pas dans le standard), c'est juste de l'explication ad-hoc. La comparaison avec le polymorphisme n'a pas de sens. Le concept "object" est clairement définie (https://en.cppreference.com/w/cpp/language/object) et cela correspond bien a l'objet qui est créé par une expression lambda. "Function object" est aussi clairement définie (https://en.cppreference.com/w/cpp/ut...ity/functional), c'est juste un objet qui peut être appelé. C'est juste toi qui créé ce concept de objet donnée et qui rend ambiguë les choses.

    Bref. Je pense qu'on a donné nos points de vue sur la question initiale de Bktero. (Pour résumer, soit utiliser const pour suivre la guideline "const par defaut". Soit pas const parce qu'on a pris l'habitude de faire comme ca. Et koala qui avance un argument technique contre const, que chacun est libre de juger de la pertinence) . Ca sert à rien de tourner en rond plus longtemps.