Come vengono rappresentati e passati i lambda di C ++ 11?

Passare un lambda è davvero facile in c ++ 11:

func( []( int arg ) { // code } ) ; 

Ma mi chiedo, qual è il costo di far passare una lambda a una funzione come questa? Cosa succede se func passa il lambda ad altre funzioni?

 void func( function f ) { doSomethingElse( f ) ; } 

Il passaggio del lambda è costoso? Poiché un object function può essere assegnato a 0,

 function f = 0 ; // 0 means "not init" 

mi porta a pensare che gli oggetti della funzione tipo agiscano come puntatori. Ma senza l’uso di new , significa che potrebbero essere come struct o classi con valore tipizzato, che per impostazione predefinita impilano l’allocazione e la copia membro-saggio.

Come viene passato un “corpo di codice” C ++ 11 e un gruppo di variabili catturate quando si passa un object funzione “in base al valore”? C’è un sacco di copie in eccesso del codice? Dovrei contrassegnare ogni object function passato con const& modo che non venga fatta una copia:

 void func( const function& f ) { } 

O gli oggetti funzione in qualche modo passano in modo diverso rispetto alle normali strutture C ++?

Disclaimer: la mia risposta è un po ‘semplificata rispetto alla realtà (ho messo da parte alcuni dettagli) ma la grande immagine è qui. Inoltre, lo Standard non specifica in modo completo come deve essere implementata interndas o std::function internamente (l’implementazione ha una certa libertà) quindi, come ogni discussione sui dettagli di implementazione, il compilatore può o meno farlo esattamente in questo modo.

Ma ancora una volta, questo è un argomento abbastanza simile a VTables: lo Standard non richiede molto, ma qualsiasi compilatore ragionevole è ancora abbastanza probabile che lo faccia in questo modo, quindi credo che valga la pena di scavarci un po ‘. 🙂


lambda

Il modo più semplice per implementare un lambda è una specie di struct anonima:

 auto lambda = [](Args...) -> Return { /*...*/ }; // roughly equivalent to: struct { Return operator ()(Args...) { /*...*/ } } lambda; // instance of the anonymous struct 

Proprio come qualsiasi altra class, quando passi le sue istanze intorno a te non devi mai copiare il codice, solo i dati effettivi (qui, nessuno affatto).


Gli oggetti catturati dal valore vengono copiati nella struct :

 Value v; auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ }; // roughly equivalent to: struct Temporary { // note: we can't make it an anonymous struct any more since we need // a constructor, but that's just a syntax quirk const Value v; // note: capture by value is const by default unless the lambda is mutable Temporary(Value v_) : v(v_) {} Return operator ()(Args...) { /*... use v, captured by value...*/ } } lambda(v); // instance of the struct 

Di nuovo, passarlo significa solo che si passa il dato ( v ) non il codice stesso.


Allo stesso modo, gli oggetti catturati per riferimento sono referenziati nella struct :

 Value v; auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ }; // roughly equivalent to: struct Temporary { Value& v; // note: capture by reference is non-const Temporary(Value& v_) : v(v_) {} Return operator ()(Args...) { /*... use v, captured by reference...*/ } } lambda(v); // instance of the struct 

Questo è più o meno tutto quando si tratta di lambda stessi (tranne i pochi dettagli di implementazione che ho ricevuto, ma che non sono rilevanti per capire come funziona).


std::function

std::function è un generico wrapper attorno a qualsiasi tipo di functor (lambda, funzioni standalone / statico / membro, classi di functor come quelle che ho mostrato, …).

Gli interni di std::function sono piuttosto complicati perché devono supportare tutti questi casi. A seconda del tipo esatto di funtore, ciò richiede almeno i seguenti dati (dare o prendere i dettagli di implementazione):

  • Un puntatore a una funzione standalone / statica.

O,

  • Un puntatore a una copia [vedi nota sotto] del funtore (assegnata dynamicmente per consentire qualsiasi tipo di funtore, come l’hai giustamente notato).
  • Un puntatore alla funzione membro da chiamare.
  • Un puntatore a un allocatore che sia in grado sia di copiare il functor che di se stesso (poiché qualsiasi tipo di functor può essere usato, il pointer-to-functor deve essere void* e quindi ci deve essere un tale meccanismo – probabilmente usando il polimorfismo conosciuto anche come Metodi di class base + virtuale, la class derivata generata localmente nei template function(Functor) ).

Dal momento che non sa in anticipo quale tipo di functor dovrà memorizzare (e ciò è reso evidente dal fatto che std::function può essere riassegnata) allora deve affrontare tutti i possibili casi e prendere la decisione al momento dell’esecuzione.

Nota: non so dove lo standard lo imponga ma questa è sicuramente una nuova copia, il functor sottostante non è condiviso:

 int v = 0; std::function f = [=]() mutable { std::cout << v++ << std::endl; }; std::function g = f; f(); // 0 f(); // 1 g(); // 0 g(); // 1 

Quindi, quando si passa una std::function attorno ad essa coinvolge almeno quei quattro puntatori (e in effetti su GCC 4.7 64 bit sizeof(std::function è 32 che è quattro puntatori a 64 bit) e facoltativamente un dynamicmente copia assegnata del funtore (che, come ho già detto, contiene solo gli oggetti catturati, non si copia il codice ).


Rispondi alla domanda

qual è il costo del passaggio di una lambda a una funzione come questa? [contesto della domanda: in base al valore ]

Bene, come potete vedere dipende principalmente dal vostro functor (o da una struct functor fatta a mano o da una lambda) e dalle variabili che contiene. Il sovraccarico rispetto al passaggio diretto di un struct functor per valore è abbastanza trascurabile, ma è ovviamente molto più alto del passaggio di un struct functor per riferimento.

Dovrei contrassegnare ogni object funzione passato con const& modo che non venga fatta una copia?

Temo che sia molto difficile rispondere in modo generico. A volte vorrai passare per riferimento const , a volte per valore, a volte per riferimento rvalue in modo da poterlo spostare. Dipende davvero dalla semantica del tuo codice.

Le regole riguardo a quale scegliere dovrebbero essere un argomento completamente diverso IMO, ricorda solo che sono uguali a qualsiasi altro object.

Ad ogni modo, ora hai tutte le chiavi per prendere una decisione informata (di nuovo, a seconda del tuo codice e della sua semantica ).

Vedi anche l’ implementazione lambda del C ++ 11 e il modello di memoria

Un’espressione lambda è proprio questo: un’espressione. Una volta compilato, risulta in un object di chiusura in fase di esecuzione.

5.1.2 Espressioni Lambda [expr.prim.lambda]

La valutazione di un’espressione lambda risulta in un valore provvisorio provvisorio (12.2). Questo temporaneo è chiamato l’object di chiusura.

L’object stesso è definito dall’implementazione e può variare dal compilatore al compilatore.

Ecco l’implementazione originale di lambda in clang https://github.com/faisalv/clang-glambda

Se il lambda può essere fatto come una funzione semplice (cioè non cattura nulla), allora è fatto esattamente allo stesso modo. Soprattutto come standard richiede che sia compatibile con il puntatore vecchio stile con la stessa firma. [EDIT: non è preciso, vedi la discussione nei commenti]

Per il resto spetta all’implementazione, ma non mi preoccuperei più avanti. L’implementazione più semplice non fa altro che portare le informazioni in giro. Esattamente quanto richiesto per la cattura. Quindi l’effetto sarebbe lo stesso come se lo facessi manualmente creando una class. O usa qualche variante std :: bind.