Próbuję zrozumieć / wyjaśnić kod kodu, który jest generowany, gdy przechwytywania są przekazywane do lambdas, szczególnie w uogólnionych przechwytywaniach init dodanych w C ++ 14.
Podaj poniższe przykłady kodu wymienione poniżej. Moje obecne rozumienie tego, co wygeneruje kompilator.
Przypadek 1: przechwytywanie według wartości / przechwytywanie domyślne według wartości
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Odpowiada to:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int x) : __x{x}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Istnieje więc wiele kopii, jedna do skopiowania do parametru konstruktora, a druga do elementu członkowskiego, co byłoby drogie w przypadku typów takich jak wektor itp.
Przypadek 2: przechwytywanie przez odniesienie / domyślne przechwytywanie przez odniesienie
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Odpowiada to:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int& x) : x_{x}{}
void operator()() const { std::cout << x << std::endl;}
private:
int& x_;
};
Parametr jest referencją, a element jest referencją, więc nie ma kopii. Ładne dla typów takich jak wektor itp.
Przypadek 3:
Uogólnione przechwytywanie init
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Rozumiem, że jest to podobne do przypadku 1 w tym sensie, że zostało skopiowane do członka.
Domyślam się, że kompilator generuje kod podobny do ...
class __some_compiler_generated_name {
public:
__some_compiler_generated_name() : __x{33}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Również jeśli mam następujące elementy:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Jak wyglądałby konstruktor? Czy przenosi to również do członka?
Odpowiedzi:
Odpowiedź na to pytanie nie jest możliwa w kodzie. Możesz być w stanie napisać nieco „równoważny” kod, ale standard nie jest określony w ten sposób.
Zejdźmy na bok
[expr.prim.lambda]
. Pierwszą rzeczą do odnotowania jest to, że konstruktory są wymienione tylko w[expr.prim.lambda.closure]/13
:Tak więc od samego początku powinno być jasne, że konstruktorzy nie określają formalnie sposobu przechwytywania obiektów. Możesz podejść bardzo blisko (patrz odpowiedź cppinsights.io), ale szczegóły różnią się (zwróć uwagę, że kod w tej odpowiedzi dla przypadku 4 nie kompiluje się).
Oto główne standardowe klauzule potrzebne do omówienia przypadku 1:
[expr.prim.lambda.capture]/10
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Zastosujmy to do twojego przypadku 1:
Typ zamknięcia tej lambdy będzie miał nienazwany element danych niestatycznych (nazwijmy go
__x
) typuint
(ponieważx
nie jest ani odniesieniem, ani funkcją), a dostęp dox
wewnątrz ciała lambda jest przekształcany w dostęp do__x
. Kiedy oceniamy wyrażenie lambda (tj. Przy przypisywaniu dolambda
), inicjalizujemy bezpośrednio za__x
pomocąx
.Krótko mówiąc, ma miejsce tylko jedna kopia . Konstruktor typu zamknięcia nie jest zaangażowany i nie jest możliwe wyrażenie tego w „normalnym” języku C ++ (należy zauważyć, że typ zamknięcia również nie jest typem zagregowanym ).
Przechwytywanie referencji obejmuje
[expr.prim.lambda.capture]/12
:Jest jeszcze jeden akapit na temat przechwytywania referencji, ale nigdzie tego nie robimy.
W przypadku 2:
Nie wiemy, czy członek jest dodawany do typu zamknięcia.
x
w ciele lambda może po prostu bezpośrednio odnosić się nax
zewnątrz. To zależy od kompilatora, aby to rozgryźć i zrobi to w jakiejś formie języka pośredniego (który różni się od kompilatora do kompilatora), a nie w źródłowej transformacji kodu C ++.Przechwytywanie początkowe opisano szczegółowo w
[expr.prim.lambda.capture]/6
:Biorąc to pod uwagę, spójrzmy na przypadek 3:
Jak powiedziano, wyobraź sobie, że jest to zmienna tworzona przez
auto x = 33;
i jawnie przechwytywana przez kopię. Ta zmienna jest „widoczna” tylko w ciele lambda. Jak zauważono[expr.prim.lambda.capture]/15
wcześniej, inicjalizacja odpowiedniego elementu typu zamknięcia (__x
dla potomności) następuje przez podany inicjator po ocenie ekspresji lambda.Aby uniknąć wątpliwości: nie oznacza to, że rzeczy są tutaj inicjowane dwukrotnie.
auto x = 33;
Jest „jak gdyby” dziedziczyć semantyki prostych przechwytywania i opisane inicjalizacji modyfikacja tych semantyki. Następuje tylko jedna inicjalizacja.Dotyczy to również przypadku 4:
Element typu zamknięcia jest inicjowany przez
__p = std::move(unique_ptr_var)
obliczenie wyrażenia lambda (tj. Kiedyl
jest przypisany). Dostęp dop
w ciele lambda przekształca się w dostęp do__p
.TL; DR: Wykonywana jest tylko minimalna liczba kopii / inicjalizacji / ruchów (jak można się spodziewać / oczekiwać). Zakładam, że lambdas nie są określone w kategoriach transformacji źródłowej (w przeciwieństwie do innych cukru syntaktycznego) właśnie dlatego , że wyrażanie rzeczy w kategoriach konstruktorów wymagałoby zbędnych operacji.
Mam nadzieję, że to rozwiąże obawy wyrażone w pytaniu :)
źródło
Przypadek 1
[x](){}
: Wygenerowany konstruktor zaakceptuje swój argument poprzez ewentualnieconst
kwalifikowane odwołanie, aby uniknąć niepotrzebnych kopii:Przypadek 2
[x&](){}
: Twoje założenia tutaj są prawidłowe,x
są przekazywane i przechowywane przez odniesienie.Przypadek 3
[x = 33](){}
: Znowu poprawny,x
jest inicjowany przez wartość.Przypadek 4
[p = std::move(unique_ptr_var)]
: Konstruktor będzie wyglądał następująco:więc tak,
unique_ptr_var
„przenosi się” do zamknięcia. Zobacz także Scott Meyer Item 32 w Effective Modern C ++ („Użyj funkcji przechwytywania init, aby przenosić obiekty do zamknięć”).źródło
const
-kwalifikowany” Dlaczego?const
nie może tu zaszkodzić z powodu niejasności / lepszego dopasowania, gdy nie jestconst
itp. W każdym razie, czy uważasz, że powinienem usunąćconst
?Nie ma potrzeby spekulacji przy użyciu cppinsights.io .
Przypadek 1:
Kod
Kompilator generuje
Przypadek 2:
Kod
Kompilator generuje
Przypadek 3:
Kod
Kompilator generuje
Przypadek 4 (nieoficjalnie):
kod
Kompilator generuje
I wierzę, że ten ostatni fragment kodu odpowiada na twoje pytanie. Ruch występuje, ale nie [technicznie] w konstruktorze.
Same zdjęcia nie są
const
, ale widać, żeoperator()
funkcja jest. Oczywiście, jeśli chcesz zmodyfikować przechwytywania, oznacz lambda jakomutable
.źródło