2013-06-04 8 views
30

passaggio di un lambda è veramente facile in C++ 11:Come vengono rappresentati e passati i lambda di C++ 11?

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

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

void func(function< void (int arg) > f) { 
    doSomethingElse(f) ; 
} 

Il passaggio della lambda è costoso? Dal momento che un oggetto function può essere assegnato 0,

function< void (int arg) > f = 0 ; // 0 means "not init" 

mi porta a pensare che la funzione degli oggetti tipo di agire come puntatori. Ma senza l'uso di new, significa che potrebbero essere come il valore struct o classi, 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 oggetto funzione "in base al valore"? C'è un sacco di copie in eccesso del codice? Dovrei avere per marcare ogni function oggetto passato con const& in modo che una copia non è fatta:

void func(const function< void (int arg) >& f) { 
} 

O fare funzione di oggetti in qualche modo passare in modo diverso rispetto ai normali struct C++?

+0

Buona domanda, vorrebbe anche sapere :) – Xaqq

risposta

30

Disclaimer: la mia risposta è un po 'semplificata rispetto alla realtà (ho messo da parte alcuni dettagli) ma il quadro generale è qui. Inoltre, lo standard non specifica in modo completo come interndas o std::function devono essere implementati 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 un anonimo struct:

auto lambda = [](Args...) -> Return { /*...*/ }; 

// roughly equivalent to: 
struct { 
    Return operator()(Args...) { /*...*/ } 
} 
lambda; // instance of the anonymous struct 

Proprio come qualsiasi altra classe, quando si passa le sue istanze intorno a voi mai copiare il codice, solo i dati effettivi (qui, nessuno).


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 

Ancora, passando attorno significa soltanto che si passa i dati (v) non il codice stesso.


Allo stesso modo, gli oggetti catturati con riferimento viene fatto riferimento nel 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 è praticamente tutto quando si tratta di lambda stessi (tranne i pochi dettagli di implementazione ho omesso, ma che non sono rilevanti per capire come funziona).


std::function

std::function è un involucro generico attorno a qualsiasi tipo di funtore (lambda, funzioni standalone/statico/utente, classi Functor come quelli che hanno mostrato, ...).

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

  • Un puntatore a una funzione autonoma/statica.

Oppure,

  • Un puntatore a una copia [vedi nota sotto] del funtore (allocata dinamicamente per consentire qualsiasi tipo di funtore, come ha giustamente notato esso).
  • Un puntatore alla funzione membro da chiamare.
  • Un puntatore a un allocatore che è in grado sia di copiare il functor che di se stesso (poiché qualsiasi tipo di functor può essere utilizzato, il puntatore-a-functor deve essere void* e quindi ci deve essere un tale meccanismo - probabilmente usando polimorfismo noto come metodo base + metodi virtuali, la classe derivata viene generata localmente nei costruttori template<class Functor> function(Functor)).

Dal momento che non sapere in anticipo che tipo di funtore dovrà memorizzare (e questo è reso evidente dal fatto che std::function possono essere riassegnati) allora deve far fronte a tutti i casi possibili e prendere la decisione in fase di esecuzione.

Nota: non so dove mandati standard, ma questo è sicuramente una nuova copia, il funtore sottostante non è condivisa:

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

f(); // 0 
f(); // 1 
g(); // 0 
g(); // 1 

Così, quando si passa un std::function intorno coinvolge almeno quei quattro puntatori (e in effetti su GCC 4.7 64 bit sizeof(std::function<void()> è 32 che è quattro puntatori a 64 bit) e facoltativamente una copia allocata dinamicamente del functor (che, come ho già detto, contiene solo gli oggetti catturati, non copiare il codice).


Risposta alla domanda

qual è il costo di passare un lambda per una funzione come questa?[contesto della questione: per valore]

Bene, come si può vedere dipende principalmente dalla vostra funtore (una fatta a mano struct functor o un lambda) e le variabili in esso contenuti. Il overhead rispetto al passaggio diretto di un valore di un valore di struct è alquanto trascurabile, ma è ovviamente molto più alto del passaggio di un functor struct per riferimento.

Dovrei avere per marcare ogni oggetto funzione passata con const& in modo che una copia non è fatta?

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

Le regole su cui si dovrebbe scegliere sono un argomento completamente diverso IMO, ricorda solo che sono gli stessi di qualsiasi altro oggetto.

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

1

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

Per il resto è l'implementazione, ma non mi preoccuperei. 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 classe. O usa qualche variante std :: bind.

+0

"* standard richiede che sia compatibile con il vecchio stile puntatore a funzione con lo stesso firma * "=> cura di citare? Non lo sapevo (che di per sé non è sorprendente). – syam

+0

Nevermind, trovato: 5.1.2-6 * Il tipo di chiusura per un'espressione lambda senza lambda-capture ha una funzione di conversione const non-public pubblica non-virtuale per puntare a una funzione che ha lo stesso parametro e tipi di ritorno del Operatore di chiamata di funzione del tipo di chiusura.Il valore restituito da questa funzione di conversione deve essere l'indirizzo di una funzione che, invocata, ha lo stesso effetto del richiamo dell'operatore di chiamata di funzione del tipo di chiusura. * ==> Non è necessario che una lambda non acquisibile sia implementata come indipendente funzione, deve solo fornire una funzione di conversione. – syam

+0

speriamo che ciò non contraddica quello che ho affermato ;-) –

3

Vedi anche C++11 lambda implementation and memory model

Un lambda-espressione è proprio questo: un'espressione. Una volta compilato, risulta in un oggetto di chiusura in fase di esecuzione.

5.1.2 Espressioni lambda [expr.prim.lambda]

La valutazione di un risultato lambda-espressione in un prvalue temporanea (12.2). Questo temporaneo è chiamato l'oggetto di chiusura.

L'oggetto 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