Résolution de la surcharge ambiguë sur le pointeur de fonction et std :: function pour un lambda en utilisant +

93

Dans le code suivant, le premier appel à fooest ambigu et échoue par conséquent à compiler.

Le second, avec l'ajout +avant le lambda, résout la surcharge du pointeur de fonction.

#include <functional>

void foo(std::function<void()> f) { f(); }
void foo(void (*f)()) { f(); }

int main ()
{
    foo(  [](){} ); // ambiguous
    foo( +[](){} ); // not ambiguous (calls the function pointer overload)
}

Que fait la +notation ici?

Steve Lorimer
la source

Réponses:

98

Le +dans l'expression +[](){}est l' +opérateur unaire . Il est défini comme suit dans [expr.unary.op] / 7:

L'opérande de l' +opérateur unaire doit avoir le type arithmétique, énumération sans portée ou pointeur et le résultat est la valeur de l'argument.

Le lambda n'est pas de type arithmétique etc., mais il peut être converti:

[expr.prim.lambda] / 3

Le type de l' expression lambda est un [...] type de classe non-union unique et sans nom - appelé type de fermeture - dont les propriétés sont décrites ci-dessous.

[expr.prim.lambda] / 6

Le type de fermeture pour un lambda-expression sans lambda-capture a une publicnon virtualnon explicit constfonction de conversion de pointeur vers une fonction ayant les mêmes paramètres et types de retour comme opérateur d'appel de fonction du type de fermeture. La valeur renvoyée par cette fonction de conversion doit être l'adresse d'une fonction qui, lorsqu'elle est invoquée, a le même effet que l'appel de l'opérateur d'appel de fonction du type de fermeture.

Par conséquent, l'unaire +force la conversion vers le type de pointeur de fonction, qui est pour ce lambda void (*)(). Par conséquent, le type de l'expression +[](){}est ce type de pointeur de fonction void (*)().

La deuxième surcharge void foo(void (*f)())devient une correspondance exacte dans le classement pour la résolution de surcharge et est donc choisie sans ambiguïté (car la première surcharge n'est PAS une correspondance exacte).


Le lambda [](){}peut être converti en std::function<void()>via le ctor de modèle non explicite de std::function, qui prend tout type qui remplit les conditions Callableet CopyConstructible.

Le lambda peut également être converti en void (*)()via la fonction de conversion du type de fermeture (voir ci-dessus).

Les deux sont des séquences de conversion définies par l'utilisateur et du même rang. C'est pourquoi la résolution de surcharge échoue dans le premier exemple en raison d'une ambiguïté.


Selon Cassio Neri, soutenu par un argument de Daniel Krügler, cette +astuce unaire devrait être un comportement spécifié, c'est-à-dire que vous pouvez vous y fier (voir discussion dans les commentaires).

Néanmoins, je recommanderais d'utiliser un cast explicite vers le type de pointeur de fonction si vous voulez éviter l'ambiguïté: vous n'avez pas besoin de demander à SO ce que cela fait et pourquoi cela fonctionne;)

dyp
la source
3
Les pointeurs de fonction membre @Fred AFAIK ne peuvent pas être convertis en pointeurs de fonction non membre, sans parler des valeurs de fonction. Vous pouvez lier une fonction membre via std::bindà un std::functionobjet qui peut être appelé de la même manière qu'une fonction lvalue.
dyp le
2
@DyP: Je pense que nous pouvons nous fier au plus délicat. En effet, supposons qu'une implémentation s'ajoute operator +()à un type de fermeture sans état. Supposons que cet opérateur renvoie autre chose que le pointeur vers la fonction vers laquelle le type de fermeture est converti. Ensuite, cela modifierait le comportement observable d'un programme qui viole 5.1.2 / 3. S'il vous plaît, faites-moi savoir si vous êtes d'accord avec ce raisonnement.
Cassio Neri
2
@CassioNeri Oui, c'est là que je ne suis pas sûr. Je suis d'accord que le comportement observable pourrait changer lors de l'ajout d'un operator +, mais cela se compare à la situation où il n'y a pas operator +pour commencer. Mais il n'est pas spécifié que le type de fermeture ne doit pas avoir de operator +surcharge. "Une implémentation peut définir le type de fermeture différemment de ce qui est décrit ci-dessous à condition que cela ne modifie pas le comportement observable du programme autrement que par [...]" mais IMO l' ajout d' un opérateur ne change pas le type de fermeture en quelque chose de différent de ce que est "décrit ci-dessous".
dyp le
3
@DyP: La situation où il n'y a pas operator +()est exactement celle décrite par la norme. La norme permet à une implémentation de faire quelque chose de différent de ce qui est spécifié. Par exemple, ajouter operator +(). Cependant, si cette différence est observable par un programme, alors elle est illégale. Une fois, j'ai demandé dans comp.lang.c ++. Modéré si un type de fermeture pouvait ajouter un typedef pour result_typeet l'autre typedefsrequis pour les rendre adaptables (par exemple par std::not1). On m'a dit que ce n'était pas possible parce que c'était observable. Je vais essayer de trouver le lien.
Cassio Neri
6
VS15 vous donne cette erreur amusante: test.cpp (543): erreur C2593: 'opérateur +' est ambigu t \ test.cpp (543): note: pourrait être 'opérateur C ++ intégré + (void (__cdecl *) (void )) 't \ test.cpp (543): note: ou' opérateur C ++ intégré + (void (__stdcall *) (void)) 't \ test.cpp (543): note: ou' opérateur C ++ intégré + (void (__fastcall *) (void)) 't \ test.cpp (543): note: ou' opérateur C ++ intégré + (void (__vectorcall *) (void)) 't \ test.cpp (543): note : en essayant de faire correspondre la liste d'arguments '(wmain :: <lambda_d983319760d11be517b3d48b95b3fe58>) test.cpp (543): erreur C2088:' + ': illégal pour la classe
Ed Lambert