Non è possibile rispondere completamente a questa domanda nel codice. Potresti essere in grado di scrivere un codice un po '"equivalente", ma lo standard non è specificato in questo modo.
Detto questo, tuffiamoci dentro [expr.prim.lambda]
. La prima cosa da notare è che i costruttori sono menzionati solo in [expr.prim.lambda.closure]/13
:
Il tipo di chiusura associato a un'espressione lambda non ha un costruttore predefinito se l' espressione lambda ha una cattura lambda e un costruttore predefinito predefinito in caso contrario. Ha un costruttore di copia predefinito e un costruttore di spostamento predefinito ([class.copy.ctor]). Ha un operatore di assegnazione copia cancellato se l' espressione lambda ha una cattura lambda e copia predefinita e sposta gli operatori di assegnazione altrimenti ([class.copy.assign]). [ Nota: queste funzioni speciali dei membri sono implicitamente definite come al solito e potrebbero quindi essere definite come eliminate. - nota finale ]
Quindi, a prima vista, dovrebbe essere chiaro che i costruttori non sono formalmente come vengono definiti gli oggetti catturati. Puoi avvicinarti abbastanza (vedi la risposta cppinsights.io), ma i dettagli differiscono (nota come il codice in quella risposta per il caso 4 non viene compilato).
Queste sono le principali clausole standard necessarie per discutere il caso 1:
[expr.prim.lambda.capture]/10
[...]
Per ogni entità acquisita dalla copia, un membro di dati non statici senza nome viene dichiarato nel tipo di chiusura. L'ordine di dichiarazione di questi membri non è specificato. Il tipo di un tale membro di dati è il tipo di riferimento se l'entità è un riferimento a un oggetto, un riferimento lvalue al tipo di funzione di riferimento se l'entità è un riferimento a una funzione o il tipo di entità acquisita corrispondente in caso contrario. Un membro di un'unione anonima non può essere catturato da una copia.
[expr.prim.lambda.capture]/11
Ogni espressione id all'interno dell'istruzione composta di un'espressione lambda che è un uso strano di un'entità catturata dalla copia viene trasformata in un accesso al corrispondente membro dati senza nome del tipo di chiusura. [...]
[expr.prim.lambda.capture]/15
Quando viene valutata l'espressione lambda, le entità acquisite dalla copia vengono utilizzate per inizializzare direttamente ciascun membro di dati non statico corrispondente dell'oggetto di chiusura risultante e i membri di dati non statici corrispondenti alle init-acquisizioni vengono inizializzati come indicato dall'inizializzatore corrispondente (che può essere l'inizializzazione di copia o diretta). [...]
Appliciamo questo al tuo caso 1:
Caso 1: acquisizione per valore / acquisizione predefinita per valore
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Il tipo di chiusura di questo lambda avrà un membro di dati non statico senza nome (chiamiamolo così __x
) di tipo int
(poiché x
non è né un riferimento né una funzione) e gli accessi x
all'interno del corpo lambda vengono trasformati in accessi __x
. Quando valutiamo l'espressione lambda (cioè quando assegniamo a lambda
), inizializziamo direttamente __x
con x
.
In breve, ha luogo una sola copia . Il costruttore del tipo di chiusura non è coinvolto e non è possibile esprimerlo in C ++ "normale" (si noti che il tipo di chiusura non è neanche un tipo aggregato ).
La cattura di riferimento prevede [expr.prim.lambda.capture]/12
:
Un'entità viene acquisita per riferimento se viene acquisita in modo implicito o esplicito ma non acquisita dalla copia. Non è specificato se i membri di dati non statici senza nome aggiuntivi vengano dichiarati nel tipo di chiusura per le entità acquisite per riferimento. [...]
C'è un altro paragrafo sull'acquisizione di riferimenti di riferimenti ma non lo stiamo facendo da nessuna parte.
Quindi, per il caso 2:
Caso 2: acquisizione per riferimento / acquisizione predefinita per riferimento
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Non sappiamo se un membro viene aggiunto al tipo di chiusura. x
nel corpo lambda potrebbe riferirsi direttamente x
all'esterno. Questo dipende dal compilatore, e lo farà in una forma di linguaggio intermedio (che differisce da compilatore a compilatore), non una trasformazione di origine del codice C ++.
Le acquisizioni di Init sono dettagliate in [expr.prim.lambda.capture]/6
:
Una init-capture si comporta come se dichiarasse e catturasse esplicitamente una variabile della forma la auto init-capture ;
cui regione dichiarativa è l'istruzione composta dell'espressione lambda, tranne che:
- (6.1) se l'acquisizione avviene tramite copia (vedere di seguito), il membro di dati non statici dichiarato per l'acquisizione e la variabile vengono trattati come due modi diversi di fare riferimento allo stesso oggetto, che ha la durata dei dati non statici membro e non viene eseguita alcuna copia e distruzione aggiuntive e
- (6.2) se l'acquisizione è per riferimento, la durata della variabile termina quando termina la durata dell'oggetto di chiusura.
Detto questo, diamo un'occhiata al caso 3:
Caso 3: acquisizione generalizzata di init
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Come detto, immagina questo come una variabile creata auto x = 33;
e catturata esplicitamente dalla copia. Questa variabile è "visibile" solo all'interno del corpo lambda. Come notato in [expr.prim.lambda.capture]/15
precedenza, l'inizializzazione del membro corrispondente del tipo di chiusura ( __x
per i posteri) viene eseguita dall'inizializzatore fornito dopo la valutazione dell'espressione lambda.
A scanso di equivoci: ciò non significa che qui le cose siano inizializzate due volte. Il auto x = 33;
è un "come se" di ereditare la semantica di semplici cattura, e l'inizializzazione descritta è una modifica di tali semantica. Si verifica una sola inizializzazione.
Questo riguarda anche il caso 4:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Il membro del tipo di chiusura viene inizializzato da __p = std::move(unique_ptr_var)
quando viene valutata l'espressione lambda (ovvero quando l
è assegnata a). Gli accessi p
nel corpo lambda vengono trasformati in accessi a __p
.
TL; DR: viene eseguito solo il numero minimo di copie / inizializzazioni / mosse (come si potrebbe sperare / aspettarsi). Suppongo che i lambda non siano specificati in termini di trasformazione della fonte (diversamente dagli altri zuccheri sintattici) proprio perché esprimere le cose in termini di costruttori richiederebbe operazioni superflue.
Spero che questo risolva le paure espresse nella domanda :)