Estou tentando entender / esclarecer o código de código que é gerado quando as capturas são passadas para lambdas, especialmente nas capturas init generalizadas adicionadas no C ++ 14.
Dê os seguintes exemplos de código listados abaixo: este é meu entendimento atual do que o compilador irá gerar.
Caso 1: captura por valor / captura padrão por valor
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Equivale a:
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;
};
Portanto, existem várias cópias, uma para copiar no parâmetro construtor e outra para copiar no membro, o que seria caro para tipos como vetor etc.
Caso 2: captura por referência / captura padrão por referência
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Equivale a:
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_;
};
O parâmetro é uma referência e o membro é uma referência, portanto, não há cópias. Bom para tipos como vetor etc.
Caso 3:
Captura init generalizada
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Meu entendimento é que isso é semelhante ao Caso 1, no sentido em que é copiado para o membro.
Meu palpite é que o compilador gera código semelhante ao ...
class __some_compiler_generated_name {
public:
__some_compiler_generated_name() : __x{33}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Além disso, se eu tiver o seguinte:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Como seria o construtor? Também o move para o membro?
Respostas:
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
: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
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Vamos aplicar isso ao seu caso 1:
O tipo de fechamento deste lambda terá um membro de dados não estático não nomeado (vamos chamá-lo
__x
) do tipoint
(já quex
não é uma referência nem uma função), e os acessosx
no corpo do lambda são transformados para acessos__x
. Quando avaliamos a expressão lambda (ou seja, ao atribuir alambda
), inicializamos diretamente__x
comx
.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
:Há outro parágrafo sobre a captura de referências, mas não fazemos isso em lugar algum.
Então, para o caso 2:
Não sabemos se um membro é adicionado ao tipo de fechamento.
x
no corpo lambda pode se referir diretamente aox
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
:Dado isso, vejamos o caso 3:
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:
O membro do tipo de fechamento é inicializado
__p = std::move(unique_ptr_var)
quando a expressão lambda é avaliada (ou seja, quandol
é atribuída a). Os acessosp
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 :)
fonte
Caso 1
[x](){}
: O construtor gerado aceitará seu argumento porconst
referência possivelmente qualificada para evitar cópias desnecessárias:Caso 2
[x&](){}
: Suas suposições aqui estão corretas,x
são passadas e armazenadas por referência.Caso 3
[x = 33](){}
: Novamente correto,x
é inicializado por valor.Caso 4
[p = std::move(unique_ptr_var)]
: O construtor terá a seguinte aparência:então sim, o
unique_ptr_var
é "movido para" o fechamento. Consulte também o Item 32 de Scott Meyer no Effective Modern C ++ ("Use a captura init para mover objetos para fechamentos").fonte
const
qualificado" Por quê?const
não pode doer aqui devido a alguma ambiguidade / melhor correspondência quando nãoconst
etc. De qualquer forma, você acha que devo remover oconst
?Há menos necessidade de especular, usando cppinsights.io .
Caso 1:
Código
Compilador gera
Caso 2:
Código
Compilador gera
Caso 3:
Código
Compilador gera
Caso 4 (não oficial):
código
Compilador gera
E acredito que este último pedaço de código responde à sua pergunta. Uma movimentação ocorre, mas não [tecnicamente] no construtor.
As capturas em si não são
const
, mas você pode ver que aoperator()
função é. Naturalmente, se você precisar modificar as capturas, marque o lambda comomutable
.fonte