Rozważ ten dość bezużyteczny program:
#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);
}
Zasadniczo próbujemy utworzyć lambdę, która sama się zwraca.
- MSVC kompiluje program i działa
- gcc kompiluje program i powoduje jego segfaults
- clang odrzuca program z komunikatem:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
Który kompilator ma rację? Czy istnieje naruszenie statycznego ograniczenia, UB, czy żadne z nich?
Zaktualizuj tę niewielką modyfikację zaakceptowaną przez 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);
Aktualizacja 2 : Rozumiem, jak napisać funktor, który sam się zwraca, lub jak użyć kombinatora Y, aby to osiągnąć. To jest bardziej kwestia językowo-prawnicza.
Aktualizacja 3 : nie chodzi o to, czy lambda powraca w ogóle, ale o legalność tego konkretnego sposobu robienia tego.
Powiązane pytanie: lambda w C ++ zwraca samą siebie .
auto& self
co eliminuje problem z wiszącymi referencjami.Odpowiedzi:
Program jest źle sformułowany (clang is right) per [dcl.spec.auto] / 9 :
Zasadniczo, odliczenie typu zwracanego wewnętrznej lambdy zależy od siebie (jednostka, o której tu mowa, jest operatorem wywołania) - musisz więc jawnie podać typ zwracany. W tym konkretnym przypadku jest to niemożliwe, ponieważ potrzebujesz typu wewnętrznej lambdy, ale nie możesz jej nazwać. Ale są inne przypadki, w których próba wymuszenia rekurencyjnych lambd, takich jak ta, może zadziałać.
Nawet bez tego masz zwisające odniesienie .
Pozwólcie, że rozwinę trochę więcej, po omówieniu z kimś znacznie mądrzejszym (tj. TC). Istnieje ważna różnica między oryginalnym kodem (nieco zmniejszona) a proponowaną nową wersją (również zmniejszoną):
I to jest to, że wewnętrzna ekspresja
self(self)
nie jest zależna odf1
, aleself(self, p)
jest zależna odf2
. Gdy wyrażenia są niezależne, można ich używać ... ochoczo ( [temp.res] / 8 , np. Jakistatic_assert(false)
jest twardy błąd niezależnie od tego, czy szablon, w którym się znajduje, jest instancji, czy nie).Dla
f1
, kompilatora (jak, powiedzmy, dzyń) może próbować wystąpienia tego chętnie. Znasz wydedukowany typ zewnętrznej lambdy, gdy dojdziesz do tego;
w punkcie#2
powyżej (jest to typ wewnętrznej lambdy), ale próbujemy użyć go wcześniej (pomyśl o tym jak w punkcie#1
) - próbujemy używać go, gdy nadal analizujemy wewnętrzną lambdę, zanim dowiemy się, jaki jest jej typ. To jest sprzeczne z dcl.spec.auto/9.Jednak
f2
nie możemy próbować gorliwie tworzyć instancji, ponieważ jest to zależne. Instancję możemy utworzyć tylko w momencie użycia, kiedy to wiemy już wszystko.Aby naprawdę zrobić coś takiego, potrzebujesz kombinatora y . Realizacja z artykułu:
A czego chcesz to:
źródło
Edycja : Wydaje się, że istnieją pewne kontrowersje dotyczące tego, czy ta konstrukcja jest ściśle poprawna zgodnie ze specyfikacją C ++. Przeważająca opinia wydaje się być nieprawidłowa. Zobacz inne odpowiedzi, aby uzyskać dokładniejszą dyskusję. Pozostała część odpowiedzi ma zastosowanie, jeśli konstrukcja jest prawidłowa; ulepszony kod poniżej działa z MSVC ++ i gcc, a OP opublikował dodatkowo zmodyfikowany kod, który działa również z clang.
Jest to niezdefiniowane zachowanie, ponieważ wewnętrzna lambda przechwytuje parametr
self
przez odniesienie, aleself
wychodzi poza zakres poreturn
linii 7. Zatem, gdy zwrócona lambda jest wykonywana później, uzyskuje dostęp do odniesienia do zmiennej, która wyszła poza zakres.Uruchomienie programu z
valgrind
ilustruje to:Zamiast tego możesz zmienić zewnętrzną lambdę, aby przyjmowała siebie przez odniesienie zamiast według wartości, unikając w ten sposób zbędnych kopii, a także rozwiązując problem:
To działa:
źródło
self
odniesienia?self
odniesienie, ten problem znika , ale Clang nadal odrzuca go z innego powoduself
to uchwycone przez odniesienie!TL; DR;
brzęk jest poprawny.
Wygląda na to, że sekcja standardu, która sprawia, że ten źle sformułowany jest to [dcl.spec.auto] p9 :
Oryginalna praca
Jeśli spojrzymy na propozycję, aby dodać Y Combinator do biblioteki standardowej , zapewni ona działające rozwiązanie:
i wyraźnie mówi, że twój przykład nie jest możliwy:
i odwołuje się do dyskusji, w której Richard Smith nawiązuje do błędu, który daje ci dzwonek :
Barry wskazał mi kolejną propozycję Lambdy rekurencyjne, która wyjaśnia, dlaczego nie jest to możliwe i działa wokół
dcl.spec.auto#9
ograniczenia, a także pokazuje metody osiągnięcia tego celu dzisiaj bez niego:źródło
self
jednak takiego bytu w programie. Nie wydaje się taką jednostką.Wygląda na to, że brzęk ma rację. Rozważmy uproszczony przykład:
Przejdźmy przez to jak kompilator (trochę):
it
jestLambda1
z operatorem wywołania szablonu.it(it);
wyzwala wystąpienie operatora połączeniaauto
, więc musimy to wydedukować.Lambda1
.self(self)
self(self)
to jest dokładnie to, od czego zaczęliśmy!W związku z tym nie można wywnioskować typu.
źródło
Lambda1::operator()
to po prostuLambda2
. Następnie w tym wewnętrznym wyrażeniu lambda znany jest również zwracany typself(self)
wywołania . Możliwe, że formalne reguły stoją na przeszkodzie w dokonaniu tej trywialnej dedukcji, ale przedstawiona tu logika nie. Ta logika sprowadza się do stwierdzenia. Jeśli na przeszkodzie stoją przepisy formalne, oznacza to usterkę reguł formalnych.Lambda1::operator()
Lambda2
Twój kod nie działa. Ale to robi:
Kod testowy:
Twój kod jest zarówno UB, jak i źle sformułowany, nie jest wymagana diagnostyka. Co jest zabawne; ale oba można naprawić niezależnie.
Po pierwsze, UB:
to jest UB, ponieważ zewnętrzny przyjmuje
self
wartość, a następnie wewnętrzny przechwytujeself
przez odniesienie, a następnie zwraca go poouter
zakończeniu działania. Więc segfaulting jest zdecydowanie w porządku.Poprawka:
Kod pozostaje źle sformułowany. Aby to zobaczyć, możemy rozszerzyć lambdy:
to tworzy wystąpienie
__outer_lambda__::operator()<__outer_lambda__>
:Następnie musimy określić typ zwracania
__outer_lambda__::operator()
.Przechodzimy przez to linijka po linijce. Najpierw tworzymy
__inner_lambda__
typ:A teraz spójrz tam - jego typ zwracany to
self(self)
lub__outer_lambda__(__outer_lambda__ const&)
. Ale jesteśmy w trakcie próby ustalenia zwracanego typu__outer_lambda__::operator()(__outer_lambda__)
.Nie możesz tego zrobić.
Chociaż w rzeczywistości zwracany typ
__outer_lambda__::operator()(__outer_lambda__)
nie jest w rzeczywistości zależny od zwracanego typu__inner_lambda__::operator()(int)
, C ++ nie przejmuje się dedukowaniem zwracanych typów; po prostu sprawdza kod linia po linii.I
self(self)
jest używany, zanim go wydedukowaliśmy. Źle ukształtowany program.Możemy to załatać, ukrywając na
self(self)
później:a teraz kod jest poprawny i kompiluje się. Ale myślę, że to trochę hack; po prostu użyj ycombinator.
źródło
operator()
nie może zostać wydedukowany, dopóki nie zostanie utworzony (przez wywołanie z jakimś argumentem jakiegoś typu). Tak więc ręczne przepisywanie, podobne do maszynowego, do kodu opartego na szablonie działa dobrze.Dość łatwo jest przepisać kod pod kątem klas, które kompilator wygenerowałby lub raczej powinien wygenerować dla wyrażeń lambda.
Kiedy to się skończy, staje się jasne, że głównym problemem jest tylko wiszące odniesienie, a kompilator, który nie akceptuje kodu, jest nieco trudniejszy w dziale lambda.
Przepisanie pokazuje, że nie ma zależności cyklicznych.
Wersja w pełni oparta na szablonie, odzwierciedlająca sposób, w jaki wewnętrzna lambda w oryginalnym kodzie przechwytuje element typu szablonowego:
Wydaje mi się, że jest to szablonowanie w wewnętrznej maszynerii, którego formalne reguły mają zabraniać. Jeśli zabraniają oryginalnej konstrukcji.
źródło
template< class > class Inner;
, że szablonoperator()
jest ... utworzony? Cóż, złe słowo. Pisemny? ... w trakcie,Outer::operator()<Outer>
zanim zostanie wydedukowany typ zwrotu operatora zewnętrznego. IInner<Outer>::operator()
ma wezwanie doOuter::operator()<Outer>
siebie. A to nie jest dozwolone. Teraz większość kompilatorów nie zauważa tego,self(self)
ponieważ czekają, aby wydedukować zwracany typOuter::Inner<Outer>::operator()<int>
kiedyint
jest przekazywany. Rozsądny. Ale pomija źle sformułowany kod.Innner<T>::operator()<U>
utworzenia wystąpienia tego szablonu funkcji . W końcu typ zwrotu może zależeć odU
tutaj. Nie, ale ogólnie.