Betrachten Sie dieses ziemlich nutzlose Programm:
#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);
}
Grundsätzlich versuchen wir, ein Lambda herzustellen, das sich selbst zurückgibt.
- MSVC kompiliert das Programm und es wird ausgeführt
- gcc kompiliert das Programm und es gibt Fehler
- clang lehnt das Programm mit einer Meldung ab:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Welcher Compiler ist richtig? Gibt es eine statische Einschränkungsverletzung, UB oder keine?
Update Diese geringfügige Änderung wird von clang akzeptiert:
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);
Update 2 : Ich verstehe, wie man einen Funktor schreibt, der sich selbst zurückgibt, oder wie man den Y-Kombinator verwendet, um dies zu erreichen. Dies ist eher eine Frage des Sprachrechtsanwalts.
Update 3 : Die Frage ist nicht, ob es für ein Lambda legal ist, sich generell zurückzugeben, sondern ob es legal ist, diese spezielle Methode zu verwenden.
Verwandte Frage: C ++ Lambda gibt sich zurück .
auto& self
das das baumelnde Referenzproblem beseitigt.Antworten:
Das Programm ist schlecht geformt (Klirren ist richtig) gemäß [dcl.spec.auto] / 9 :
Grundsätzlich hängt der Abzug des Rückgabetyps des inneren Lambda von sich selbst ab (die hier genannte Entität ist der Aufrufoperator) - Sie müssen also explizit einen Rückgabetyp angeben. In diesem speziellen Fall ist das unmöglich, da Sie den Typ des inneren Lambda benötigen, ihn aber nicht benennen können. Es gibt aber auch andere Fälle, in denen der Versuch, solche rekursiven Lambdas zu erzwingen, funktionieren kann.
Auch ohne das haben Sie eine baumelnde Referenz .
Lassen Sie mich noch etwas näher darauf eingehen, nachdem ich mit jemandem gesprochen habe, der viel schlauer ist (z. B. TC). Es gibt einen wichtigen Unterschied zwischen dem ursprünglichen Code (leicht reduziert) und der vorgeschlagenen neuen Version (ebenfalls reduziert):
Und das ist, dass der innere Ausdruck
self(self)
nicht abhängig istf1
, sondernself(self, p)
abhängig ist fürf2
. Wenn Ausdrücke nicht abhängig sind, können sie ... eifrig verwendet werden ( [temp.res] / 8 , z. B. wiestatic_assert(false)
ist ein schwerer Fehler, unabhängig davon, ob die Vorlage, in der sie sich befindet, instanziiert ist oder nicht).Denn
f1
ein Compiler (wie zum Beispiel Clang) kann versuchen, dies eifrig zu instanziieren. Sie kennen den abgeleiteten Typ des äußeren Lambdas, sobald Sie diesen;
Punkt#2
oben erreicht haben (es ist der Typ des inneren Lambdas), aber wir versuchen, ihn früher zu verwenden (denken Sie an den Punkt#1
) - wir versuchen es um es zu verwenden, während wir noch das innere Lambda analysieren, bevor wir wissen, um welchen Typ es sich tatsächlich handelt. Das läuft dcl.spec.auto/9 zuwider.Doch für die
f2
, können wir nicht eifrig zu instanziiert versuchen, weil es abhängig ist. Wir können nur am Verwendungsort instanziieren, bis zu welchem Punkt wir alles wissen.Um so etwas wirklich zu machen, braucht man einen y-Kombinator . Die Umsetzung aus dem Papier:
Und was Sie wollen ist:
quelle
Bearbeiten : Es scheint einige Kontroversen darüber zu geben, ob diese Konstruktion gemäß der C ++ - Spezifikation streng gültig ist. Die vorherrschende Meinung scheint zu sein, dass es nicht gültig ist. Weitere Informationen finden Sie in den anderen Antworten. Der Rest dieser Antwort gilt, wenn die Konstruktion gültig ist; Der unten optimierte Code funktioniert mit MSVC ++ und gcc, und das OP hat weiteren modifizierten Code veröffentlicht, der auch mit clang funktioniert.
Dies ist ein undefiniertes Verhalten, da das innere Lambda den Parameter als
self
Referenz erfasst , aberself
nachreturn
Zeile 7 den Gültigkeitsbereich verlässt. Wenn das zurückgegebene Lambda später ausgeführt wird, greift es auf eine Referenz auf eine Variable zu, die den Gültigkeitsbereich verlassen hat.Das Ausführen des Programms mit
valgrind
veranschaulicht dies:Stattdessen können Sie das äußere Lambda so ändern, dass es sich selbst als Referenz statt als Wert nimmt, wodurch unnötige Kopien vermieden und das Problem gelöst werden:
Das funktioniert:
quelle
self
Referenz machen?self
eine Referenz, dieses Problem geht weg , aber Clang noch lehnt es aus einem anderen Grundself
als Referenz erfasst wird!TL; DR;
Klirren ist richtig.
Es sieht so aus, als ob der Abschnitt des Standards, der diese schlecht geformt macht, [dcl.spec.auto] p9 ist :
Originalarbeit durch
Wenn wir uns den Vorschlag A Vorschlag zum Hinzufügen eines Y-Kombinators zur Standardbibliothek ansehen , bietet er eine funktionierende Lösung:
und es heißt ausdrücklich, dass Ihr Beispiel nicht möglich ist:
und es bezieht sich auf eine Diskussion, in der Richard Smith auf den Fehler anspielt, den Clang Ihnen gibt :
Barry wies mich auf den Folgeantrag Rekursive Lambdas hin, der erklärt, warum dies nicht möglich ist, die
dcl.spec.auto#9
Einschränkung umgeht und auch Methoden zeigt, um dies heute ohne sie zu erreichen:quelle
self
Scheint keine solche Entität zu sein.Es scheint, als ob Klirren richtig ist. Betrachten Sie ein vereinfachtes Beispiel:
Gehen wir es wie ein Compiler durch (ein bisschen):
it
istLambda1
mit einem Vorlagenaufrufoperator.it(it);
löst die Instanziierung des Anrufbetreibers ausauto
, daher müssen wir ihn ableiten.Lambda1
.self(self)
self(self)
Genau damit haben wir begonnen!Daher kann der Typ nicht abgeleitet werden.
quelle
Lambda1::operator()
ist einfachLambda2
. Dann ist bekannt, dass innerhalb dieses inneren Lambda-Ausdrucks auch der Rückgabetypself(self)
, ein Aufruf vonLambda1::operator()
, istLambda2
. Möglicherweise stehen die formalen Regeln dieser trivialen Folgerung im Wege, die hier vorgestellte Logik jedoch nicht. Die Logik hier läuft nur auf eine Behauptung hinaus. Wenn die formalen Regeln im Weg stehen, dann ist das ein Fehler in den formalen Regeln.Nun, Ihr Code funktioniert nicht. Aber das tut:
Testcode:
Ihr Code ist sowohl UB als auch schlecht geformt, keine Diagnose erforderlich. Welches ist lustig; aber beide können unabhängig voneinander behoben werden.
Erstens, die UB:
Dies ist UB, da der äußere
self
Wert nach Wert nimmt, der innere Wertself
nach Referenz erfasst und nachouter
Abschluss des Laufvorgangs zurückgegeben wird. Segfaulting ist also definitiv in Ordnung.Die Reparatur:
Der Code bleibt schlecht geformt. Um dies zu sehen, können wir die Lambdas erweitern:
dies instanziiert
__outer_lambda__::operator()<__outer_lambda__>
:Also müssen wir als nächstes den Rückgabetyp von bestimmen
__outer_lambda__::operator()
.Wir gehen es Zeile für Zeile durch. Zuerst erstellen wir
__inner_lambda__
Typ:Schauen Sie dort nach - der Rückgabetyp ist
self(self)
oder__outer_lambda__(__outer_lambda__ const&)
. Aber wir sind gerade dabei, den Rückgabetyp von abzuleiten__outer_lambda__::operator()(__outer_lambda__)
.Das darfst du nicht.
Während der Rückgabetyp von
__outer_lambda__::operator()(__outer_lambda__)
tatsächlich nicht vom Rückgabetyp von abhängt__inner_lambda__::operator()(int)
, ist es C ++ egal, wie Rückgabetypen abgeleitet werden. es überprüft einfach den Code Zeile für Zeile.Und
self(self)
wird verwendet, bevor wir es abgeleitet haben. Schlecht geformtes Programm.Wir können dies korrigieren, indem wir uns
self(self)
bis später verstecken :und jetzt ist der Code korrekt und kompiliert. Aber ich denke, das ist ein bisschen Hack; benutze einfach den ykombinator.
quelle
operator()
im Allgemeinen erst abgeleitet werden, wenn es instanziiert ist (indem er mit einem Argument eines bestimmten Typs aufgerufen wird). Und so funktioniert ein manuelles maschinenähnliches Umschreiben in vorlagenbasierten Code einwandfrei.Es ist einfach genug, den Code in Bezug auf die Klassen neu zu schreiben, die ein Compiler für die Lambda-Ausdrücke generieren würde oder sollte.
Wenn das erledigt ist, ist es klar, dass das Hauptproblem nur die baumelnde Referenz ist und dass ein Compiler, der den Code nicht akzeptiert, in der Lambda-Abteilung etwas herausgefordert ist.
Das Umschreiben zeigt, dass es keine zirkulären Abhängigkeiten gibt.
Eine Version mit vollständigen Vorlagen, die die Art und Weise widerspiegelt, in der das innere Lambda im Originalcode ein Element mit Vorlagen erfasst:
Ich denke, es ist diese Vorlage in der internen Maschinerie, die die formalen Regeln verbieten sollen. Wenn sie das ursprüngliche Konstrukt verbieten.
quelle
template< class > class Inner;
die Vorlageoperator()
... instanziiert ist. Nun, falsches Wort. Geschrieben? ... währendOuter::operator()<Outer>
vor der Rückgabe wird der Rückgabetyp des äußeren Operators abgeleitet. UndInner<Outer>::operator()
hat einen Anruf fürOuter::operator()<Outer>
sich. Und das ist nicht erlaubt. Jetzt bemerken die meisten Compiler das nicht ,self(self)
weil sie darauf warten, den RückgabetypOuter::Inner<Outer>::operator()<int>
für die Übergabe abzuleitenint
. Sinnvoll. Aber es fehlt die schlechte Form des Codes.Innner<T>::operator()<U>
instanziiert wird. Immerhin könnte der Rückgabetyp vom hier abhängenU
. Das tut es nicht, aber im Allgemeinen.