Generazione di codice Lambda C ++ con Init Captures in C ++ 14


9

Sto cercando di comprendere / chiarire il codice di codice che viene generato quando le acquisizioni vengono passate a lambdas, specialmente nelle acquisizioni generalizzate di init aggiunte in C ++ 14.

Fornisci i seguenti esempi di codice elencati di seguito: questa è la mia attuale comprensione di ciò che il compilatore genererà.

Caso 1: acquisizione per valore / acquisizione predefinita per valore

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

Equivarrebbe 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;
};

Quindi ci sono più copie, una da copiare nel parametro del costruttore e una da copiare nel membro, che sarebbe costosa per tipi come vector ecc.

Caso 2: acquisizione per riferimento / acquisizione predefinita per riferimento

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

Equivarrebbe 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_;
};

Il parametro è un riferimento e il membro è un riferimento, quindi nessuna copia. Bello per tipi come vettore ecc.

Caso 3:

Acquisizione inizializzata generalizzata

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

La mia comprensione è che è simile al caso 1, nel senso che viene copiato nel membro.

La mia ipotesi è che il compilatore generi codice simile a ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Anche se ho il seguente:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

Come sarebbe il costruttore? Lo sposta anche nel membro?


1
@ rafix07 In tal caso il codice di insight generato non verrà nemmeno compilato (tenta di copiare-inizializzare il membro ptr univoco dall'argomento). cppinsights è utile per avere un'idea generale, ma chiaramente non è in grado di rispondere a questa domanda qui.
Max Langhof,

Sembri supporre che ci sia una traduzione di lambda per i funzioni come primo passo della compilazione, o stai semplicemente cercando un codice equivalente (cioè lo stesso comportamento)? Il modo in cui un compilatore specifico genera codice (e quale codice genera) dipenderà dal compilatore, dalla versione, dall'architettura, dai flag, ecc. Quindi, stai chiedendo una piattaforma specifica? In caso contrario, la tua domanda non è realmente rispondibile. Diverso dal codice generato reale sarà probabilmente più efficiente dei funzioni che elenchi (ad es. Costruttori incorporati, evitando copie non necessarie, ecc.).
Sander De Dycker,

2
Se sei interessato a ciò che lo standard C ++ ha da dire al riguardo, fai riferimento a [expr.prim.lambda] . È troppo per riassumere qui come una risposta.
Sander De Dycker,

Risposte:


2

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é xnon è né un riferimento né una funzione) e gli accessi xall'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. xnel corpo lambda potrebbe riferirsi direttamente xall'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]/15precedenza, l'inizializzazione del membro corrispondente del tipo di chiusura ( __xper 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 pnel 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 :)


9

Caso 1 [x](){} : il costruttore generato accetterà il suo argomento con constriferimenti eventualmente qualificati per evitare copie non necessarie:

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

Caso 2 [x&](){} : I tuoi presupposti qui sono corretti, xsono passati e archiviati per riferimento.


Caso 3 [x = 33](){} : ancora una volta corretto, xviene inizializzato dal valore.


Caso 4 [p = std::move(unique_ptr_var)] : il costruttore apparirà così:

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

quindi sì, unique_ptr_varviene "spostato" nella chiusura. Vedi anche l'articolo 32 di Scott Meyer in Effective Modern C ++ ("Usa init capture per spostare gli oggetti nelle chiusure").


" const-qualified" Perché?
cpplearner

@cpplearner Mh, bella domanda. Immagino di averlo inserito perché uno di quegli automatismi mentali è entrato in gioco ^^ Almeno constnon può ferire qui a causa di qualche ambiguità / migliore corrispondenza quando non constecc. Comunque, pensi che dovrei rimuovere il const?
lubgr

Penso che const debba rimanere, cosa succede se l'argomento passato a è effettivamente const?
Aconcagua

Quindi stai dicendo che due costruzioni di mosse (o copia) accadono qui?
Max Langhof,

Scusate, intendo nel caso 4 (per le mosse) e nel caso 1 (per le copie). La copia della mia domanda non ha senso in base alle tue dichiarazioni (ma io metto in dubbio quelle dichiarazioni).
Max Langhof,

5

C'è meno bisogno di speculare, usando cppinsights.io .

Caso 1:
codice

#include <memory>

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

Il compilatore genera

#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:
codice

#include <iostream>
#include <memory>

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

Il compilatore genera

#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:
codice

#include <iostream>

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

Il compilatore genera

#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 (ufficiosamente):
codice

#include <iostream>
#include <memory>

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

Il compilatore genera

// 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 credo che questo ultimo pezzo di codice risponda alla tua domanda. Si verifica una mossa, ma non [tecnicamente] nel costruttore.

Le catture stesse non lo sono const, ma puoi vedere che la operator()funzione è. Naturalmente, se è necessario modificare le acquisizioni, contrassegnare la lambda come mutable.


Il codice che mostri per l'ultimo caso non viene nemmeno compilato. La conclusione "si verifica una mossa, ma non [tecnicamente] nel costruttore" non può essere supportata da quel codice.
Max Langhof,

Il Code of case 4 sicuramente si compila sul mio Mac. Sono sorpreso che il codice espanso generato da cppinsights non venga compilato. A questo punto il sito è stato abbastanza affidabile per me. Solleverò un problema con loro. EDIT: ho confermato che il codice generato non viene compilato; questo non era chiaro senza questa modifica.
svedese il

1
Link al problema in caso di interesse: github.com/andreasfertig/cppinsights/issues/258 Consiglio ancora il sito per cose come testare SFINAE e se si verifichino o meno cast impliciti.
svedese il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.