Esta pergunta não pode ser totalmente respondida no código. Você pode escrever um código "equivalente", mas o padrão não é especificado dessa maneira.
Com isso fora do caminho, vamos nos aprofundar [expr.prim.lambda]
. A primeira coisa a observar é que os construtores são mencionados apenas em [expr.prim.lambda.closure]/13
:
O tipo de fechamento associado a uma expressão lambda não terá construtor padrão se a expressão lambda tiver uma captura lambda e um construtor padrão padrão caso contrário. Ele possui um construtor de cópia padrão e um construtor de movimentação padrão ([class.copy.ctor]). Ele possui um operador de atribuição de cópia excluída se a expressão lambda tiver uma captura lambda e os operadores de atribuição de cópia e movimentação padrão, caso contrário ([class.copy.assign]). [ Nota: Essas funções-membro especiais são implicitamente definidas como de costume e, portanto, podem ser definidas como excluídas. - nota final ]
Portanto, logo de cara, deve ficar claro que os construtores não estão formalmente como a captura de objetos é definida. Você pode se aproximar bastante (consulte a resposta cppinsights.io), mas os detalhes diferem (observe como o código nessa resposta do caso 4 não é compilado).
Estas são as principais cláusulas padrão necessárias para discutir o caso 1:
[expr.prim.lambda.capture]/10
[...]
Para cada entidade capturada por cópia, um membro de dados não estático não nomeado é declarado no tipo de fechamento. A ordem de declaração desses membros não é especificada. O tipo desse membro de dados é o tipo referenciado, se a entidade for uma referência a um objeto, uma referência lvalue ao tipo de função referenciada, se a entidade for uma referência a uma função, ou o tipo da entidade capturada correspondente, caso contrário. Um membro de uma união anônima não deve ser capturado por cópia.
[expr.prim.lambda.capture]/11
Toda expressão de id na instrução composta de uma expressão lambda que é um uso odr de uma entidade capturada por cópia é transformada em um acesso ao membro de dados não nomeado correspondente do tipo de fechamento. [...]
[expr.prim.lambda.capture]/15
Quando a expressão lambda é avaliada, as entidades capturadas por cópia são usadas para inicializar diretamente cada membro de dados não estático correspondente do objeto de fechamento resultante e os membros de dados não estáticos correspondentes às capturas de init são inicializados como indicado pelo inicializador correspondente (que pode ser uma cópia ou inicialização direta). [...]
Vamos aplicar isso ao seu caso 1:
Caso 1: captura por valor / captura padrão por valor
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
O tipo de fechamento deste lambda terá um membro de dados não estático não nomeado (vamos chamá-lo __x
) do tipo int
(já que x
não é uma referência nem uma função), e os acessos x
no corpo do lambda são transformados para acessos __x
. Quando avaliamos a expressão lambda (ou seja, ao atribuir a lambda
), inicializamos diretamente __x
com x
.
Em resumo, apenas uma cópia ocorre . O construtor do tipo de fechamento não está envolvido e não é possível expressar isso em C ++ "normal" (observe que o tipo de fechamento também não é um tipo agregado ).
A captura de referência envolve [expr.prim.lambda.capture]/12
:
Uma entidade é capturada por referência se for implícita ou explicitamente capturada, mas não capturada por cópia. Não é especificado se membros de dados não estáticos adicionais sem nome são declarados no tipo de fechamento para entidades capturadas por referência. [...]
Há outro parágrafo sobre a captura de referências, mas não fazemos isso em lugar algum.
Então, para o caso 2:
Caso 2: captura por referência / captura padrão por referência
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Não sabemos se um membro é adicionado ao tipo de fechamento. x
no corpo lambda pode se referir diretamente ao x
exterior. Isso depende do compilador e o fará em alguma forma de linguagem intermediária (que difere de compilador para compilador), não uma transformação de origem do código C ++.
As capturas de inicialização são detalhadas em [expr.prim.lambda.capture]/6
:
Uma captura init se comporta como se declare e capture explicitamente uma variável da forma auto init-capture ;
cuja região declarativa seja a declaração composta da expressão lambda, exceto que:
- (6.1) se a captura for por cópia (veja abaixo), o membro de dados não estático declarado para a captura e a variável são tratados como duas maneiras diferentes de se referir ao mesmo objeto, que tem a vida útil dos dados não estáticos membro, e nenhuma cópia e destruição adicional é executada, e
- (6.2) se a captura for por referência, a vida útil da variável termina quando a vida útil do objeto de fechamento termina.
Dado isso, vejamos o caso 3:
Caso 3: Captura init generalizada
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Como afirmado, imagine isso como uma variável criada auto x = 33;
e capturada explicitamente por cópia. Essa variável é apenas "visível" dentro do corpo lambda. Como observado [expr.prim.lambda.capture]/15
anteriormente, a inicialização do membro correspondente do tipo de fechamento ( __x
para posteridade) é feita pelo inicializador fornecido após a avaliação da expressão lambda.
Para evitar dúvidas: isso não significa que as coisas sejam inicializadas duas vezes aqui. O auto x = 33;
é um "como se" para herdar a semântica de capturas simples, e a inicialização descrita é uma modificação para essa semântica. Apenas uma inicialização acontece.
Isso também abrange o caso 4:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
O membro do tipo de fechamento é inicializado __p = std::move(unique_ptr_var)
quando a expressão lambda é avaliada (ou seja, quando l
é atribuída a). Os acessos p
no corpo lambda são transformados em acessos a __p
.
TL; DR: Somente o número mínimo de cópias / inicializações / movimentos é realizado (como seria de esperar / esperado). Eu assumiria que as lambdas não são especificadas em termos de uma transformação de origem (diferente de outro açúcar sintático) exatamente porque expressar coisas em termos de construtores exigiria operações supérfluas.
Espero que isso resolva os medos expressos na pergunta :)