Geração de código Lambda em C ++ com capturas de inicialização em C ++ 14

9

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?

Blair Davidson
fonte
11
@ rafix07 Nesse caso, o código de insight gerado nem será compilado (ele tenta inicializar a cópia do membro ptr exclusivo do argumento). O cppinsights é útil para obter uma essência geral, mas claramente não é capaz de responder a essa pergunta aqui.
precisa
Você parece assumir que há uma tradução de lambda para functors como um primeiro passo da compilação, ou você está apenas procurando por código equivalente (ou seja, o mesmo comportamento)? A maneira como um compilador específico gera código (e qual código ele gera) dependerá do compilador, versão, arquitetura, sinalizadores etc. Então, você está solicitando uma plataforma específica? Caso contrário, sua pergunta não é realmente responsável. Além do código gerado, provavelmente será mais eficiente do que os functores listados (por exemplo, construtores embutidos, evitando cópias desnecessárias etc.).
Sander De Dycker 11/11/19
2
Se você estiver interessado no que o padrão C ++ tem a dizer sobre isso, consulte [expr.prim.lambda] . É demais resumir aqui como resposta.
Sander De Dycker 11/11/19

Respostas:

2

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 xnão é uma referência nem uma função), e os acessos xno 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. xno corpo lambda pode se referir diretamente ao xexterior. 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]/15anteriormente, a inicialização do membro correspondente do tipo de fechamento ( __xpara 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 pno 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 :)

Max Langhof
fonte
9

Caso 1 [x](){} : O construtor gerado aceitará seu argumento por constreferência possivelmente qualificada para evitar cópias desnecessárias:

__some_compiler_generated_name(const int& x) : x_{x}{}

Caso 2 [x&](){} : Suas suposições aqui estão corretas, xsã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:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

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").

lubgr
fonte
" constqualificado" Por quê?
Cpplearner 8/10/19
@ cpplearner Mh, boa pergunta. Acho que inseri isso porque um desses automatismos mentais entrou em ação ^^ Pelo menos constnão pode doer aqui devido a alguma ambiguidade / melhor correspondência quando não constetc. De qualquer forma, você acha que devo remover o const?
lubgr
Eu acho que const deve permanecer, e se o argumento passado para realmente for const?
Aconcagua
Então você está dizendo que duas construções de movimento (ou cópia) acontecem aqui?
Max Langhof
Desculpe, quero dizer no caso 4 (para os movimentos) e no caso 1 (para as cópias). A parte da cópia da minha pergunta não faz sentido com base nas suas declarações (mas eu as questiono).
precisa
5

Há menos necessidade de especular, usando cppinsights.io .

Caso 1:
Código

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Compilador gera

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Caso 2:
Código

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Compilador gera

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Caso 3:
Código

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Compilador gera

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Caso 4 (não oficial):
código

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Compilador gera

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

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 a operator()função é. Naturalmente, se você precisar modificar as capturas, marque o lambda como mutable.

sweenish
fonte
O código que você mostra para o último caso nem compila. A conclusão "ocorre uma movimentação, mas não [tecnicamente] no construtor" não pode ser suportada por esse código.
precisa
O Código do caso 4 certamente compila no meu Mac. Estou surpreso que o código expandido gerado do cppinsights não seja compilado. Até agora, o site tem sido bastante confiável para mim. Vou levantar um problema com eles. Edição: Confirmei que o código gerado não compila; isso não ficou claro sem esta edição.
sweenish
11
Link para o problema em caso de interesse: github.com/andreasfertig/cppinsights/issues/258 Eu ainda recomendo o site para coisas como testar o SFINAE e se ocorrerão ou não lançamentos implícitos.
sweenish