Considérez ce programme assez inutile:
#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}
Fondamentalement, nous essayons de faire un lambda qui se retourne.
- MSVC compile le programme et il s'exécute
- gcc compile le programme, et il segmente les défauts
- clang rejette le programme avec un message:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Quel compilateur a raison? Y a-t-il une violation de contrainte statique, UB ou aucun des deux?
Mettre à jour cette légère modification est acceptée par clang:
auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);
Mise à jour 2 : Je comprends comment écrire un foncteur qui se renvoie lui-même, ou comment utiliser le combinateur Y, pour y parvenir. C'est plus une question de langue-avocat.
Mise à jour 3 : la question n'est pas de savoir s'il est légal pour un lambda de se retourner en général, mais de la légalité de cette manière spécifique de le faire.
Question connexe: C ++ lambda se renvoyant .
auto& self
ce qui élimine le problème de référence pendant.Réponses:
Le programme est mal formé (clang a raison) par [dcl.spec.auto] / 9 :
Fondamentalement, la déduction du type de retour du lambda interne dépend de lui-même (l'entité nommée ici est l'opérateur d'appel) - vous devez donc fournir explicitement un type de retour. Dans ce cas particulier, c'est impossible, car vous avez besoin du type du lambda interne mais vous ne pouvez pas le nommer. Mais il y a d'autres cas où essayer de forcer des lambdas récursifs comme celui-ci peut fonctionner.
Même sans cela, vous avez une référence pendante .
Permettez-moi d'élaborer un peu plus, après avoir discuté avec quelqu'un de beaucoup plus intelligent (c.-à-d. TC) Il y a une différence importante entre le code original (légèrement réduit) et la nouvelle version proposée (également réduite):
Et c'est que l'expression intérieure
self(self)
n'est pas dépendante pourf1
, maisself(self, p)
dépendante pourf2
. Lorsque les expressions ne sont pas dépendantes, elles peuvent être utilisées ... avec empressement ( [temp.res] / 8 , par exemple commentstatic_assert(false)
est une erreur matérielle, que le modèle dans lequel il se trouve soit instancié ou non).Car
f1
, un compilateur (comme, disons, clang) peut essayer d'instancier cela avec empressement. Vous connaissez le type déduit du lambda externe une fois que vous y arrivez;
au point#2
ci-dessus (c'est le type du lambda interne), mais nous essayons de l'utiliser plus tôt que cela (pensez-y comme au point#1
) - nous essayons pour l'utiliser pendant que nous analysons encore le lambda interne, avant de savoir quel est son type. Cela va à l'encontre de dcl.spec.auto/9.Cependant, pour
f2
, nous ne pouvons pas essayer d'instancier avec empressement, car c'est dépendant. Nous ne pouvons instancier qu'au point d'utilisation, à partir duquel nous savons tout.Pour vraiment faire quelque chose comme ça, vous avez besoin d'un combinateur y . La mise en œuvre du papier:
Et ce que vous voulez, c'est:
la source
Edit : Il semble y avoir une controverse quant à savoir si cette construction est strictement valide selon la spécification C ++. L'opinion dominante semble être qu'elle n'est pas valable. Voir les autres réponses pour une discussion plus approfondie. Le reste de cette réponse s'applique si la construction est valide; le code modifié ci-dessous fonctionne avec MSVC ++ et gcc, et l'OP a publié un code modifié qui fonctionne également avec clang.
C'est un comportement indéfini, car le lambda interne capture le paramètre
self
par référence, maisself
sort de la portée après lareturn
ligne 7. Ainsi, lorsque le lambda retourné est exécuté plus tard, il accède à une référence à une variable qui est hors de portée.L'exécution du programme avec
valgrind
illustre ceci:Au lieu de cela, vous pouvez changer le lambda externe pour prendre self par référence au lieu de par valeur, évitant ainsi un tas de copies inutiles et résolvant également le problème:
Cela marche:
la source
self
référence?self
une référence, ce problème disparaît , mais Clang le rejetteself
capturé par référence!TL, DR;
clang est correct.
Il semble que la section de la norme qui rend cette mauvaise forme est [dcl.spec.auto] p9 :
Travail original à travers
Si nous examinons la proposition A Proposal to Add Y Combinator to the Standard Library, elle fournit une solution de travail:
et il dit explicitement que votre exemple n'est pas possible:
et il fait référence à une discussion dans laquelle Richard Smith fait allusion à l'erreur que le bruit vous donne :
Barry m'a indiqué la proposition de suivi Lambdas récursives qui explique pourquoi cela n'est pas possible et qui contourne la
dcl.spec.auto#9
restriction et montre également des méthodes pour y parvenir aujourd'hui sans elle:la source
self
Ne semble pas une telle entité.Il semble que clang a raison. Prenons un exemple simplifié:
Passons en revue comme un compilateur (un peu):
it
estLambda1
avec un opérateur d'appel de modèle.it(it);
déclenche l'instanciation de l'opérateur d'appelauto
, nous devons donc le déduire.Lambda1
.self(self)
self(self)
c'est exactement ce avec quoi nous avons commencé!En tant que tel, le type ne peut pas être déduit.
la source
Lambda1::operator()
est simplementLambda2
. Ensuite, dans cette expression lambda interne, le type de retour deself(self)
, un appel deLambda1::operator()
, est également connuLambda2
. Peut-être que les règles formelles font obstacle à cette déduction triviale, mais la logique présentée ici ne le fait pas. La logique ici équivaut simplement à une affirmation. Si les règles formelles font obstacle, alors c'est une faille dans les règles formelles.Eh bien, votre code ne fonctionne pas. Mais cela fait:
Code de test:
Votre code est à la fois UB et mal formé, aucun diagnostic n'est requis. Ce qui est drôle; mais les deux peuvent être fixés indépendamment.
Tout d'abord, l'UB:
c'est UB parce que externe prend
self
par valeur, puis interne captureself
par référence, puis continue à le renvoyer une fois l'outer
exécution terminée. Donc, le segfaulting est définitivement ok.Le correctif:
Le code reste mal formé. Pour voir cela, nous pouvons développer les lambdas:
cela instancie
__outer_lambda__::operator()<__outer_lambda__>
:Nous devons donc ensuite déterminer le type de retour de
__outer_lambda__::operator()
.Nous le parcourons ligne par ligne. Nous créons d'abord le
__inner_lambda__
type:Maintenant, regardez là - son type de retour est
self(self)
, ou__outer_lambda__(__outer_lambda__ const&)
. Mais nous sommes en train d'essayer de déduire le type de retour de__outer_lambda__::operator()(__outer_lambda__)
.Vous n'êtes pas autorisé à faire ça.
Alors qu'en fait le type de retour de
__outer_lambda__::operator()(__outer_lambda__)
ne dépend pas réellement du type de retour de__inner_lambda__::operator()(int)
, C ++ ne se soucie pas de la déduction des types de retour; il vérifie simplement le code ligne par ligne.Et
self(self)
est utilisé avant de le déduire. Programme mal formé.Nous pouvons corriger cela en cachant
self(self)
à plus tard:et maintenant le code est correct et compile. Mais je pense que c'est un peu de hack; utilisez simplement le ycombinator.
la source
operator()
ne peut en général être déduit tant qu'il n'est pas instancié (en étant appelé avec un argument d'un certain type). Ainsi, une réécriture manuelle de type machine vers un code basé sur un modèle fonctionne bien.Il est assez facile de réécrire le code en termes de classes qu'un compilateur générerait, ou plutôt devrait, générer pour les expressions lambda.
Lorsque cela est fait, il est clair que le problème principal est juste la référence pendante, et qu'un compilateur qui n'accepte pas le code est quelque peu contesté dans le département lambda.
La réécriture montre qu'il n'y a pas de dépendances circulaires.
Une version entièrement basée sur un modèle pour refléter la façon dont le lambda interne dans le code d'origine capture un élément qui est d'un type basé sur un modèle:
Je suppose que c'est ce modèle dans le mécanisme interne, que les règles formelles sont destinées à interdire. S'ils interdisent la construction d'origine.
la source
template< class > class Inner;
modèleoperator()
est ... instancié? Eh bien, faux mot. Écrit? ... pendantOuter::operator()<Outer>
avant que le type de retour de l'opérateur externe ne soit déduit. EtInner<Outer>::operator()
a un appel àOuter::operator()<Outer>
lui-même. Et ce n'est pas autorisé. Maintenant, la plupart des compilateurs ne remarquent pas leself(self)
parce qu'ils attendent de déduire le type de retour deOuter::Inner<Outer>::operator()<int>
quandint
est passé. Sensible. Mais il manque la mauvaise forme du code.Innner<T>::operator()<U>
soit instancié. Après tout, le type de retour pourrait dépendre de l'U
ici. Ce n'est pas le cas, mais en général.