Il lambda in questione in realtà non ha uno stato .
Esaminare:
struct lambda {
auto operator()() const { return 17; }
};
E se lo avessimo lambda f;
, è una classe vuota. Non solo quanto sopra è lambda
funzionalmente simile al tuo lambda, è (fondamentalmente) come viene implementato il tuo lambda! (Richiede anche un cast implicito all'operatore del puntatore di funzione e il nome lambda
verrà sostituito con uno pseudo-guid generato dal compilatore)
In C ++, gli oggetti non sono puntatori. Sono cose reali. Utilizzano solo lo spazio necessario per memorizzare i dati al loro interno. Un puntatore a un oggetto può essere più grande di un oggetto.
Anche se potresti pensare a quel lambda come un puntatore a una funzione, non lo è. Non è possibile riassegnare il auto f = [](){ return 17; };
a una funzione o lambda diversa!
auto f = [](){ return 17; };
f = [](){ return -42; };
quanto sopra è illegale . Non c'è spazio f
per memorizzare quale funzione verrà chiamata - quell'informazione è memorizzata nel tipo di f
, non nel valore di f
!
Se l'hai fatto:
int(*f)() = [](){ return 17; };
o questo:
std::function<int()> f = [](){ return 17; };
non stai più immagazzinando direttamente la lambda. In entrambi i casi, f = [](){ return -42; }
è legale, quindi in questi casi memorizziamo quale funzione stiamo invocando nel valore di f
. E sizeof(f)
non è più1
, ma piuttosto sizeof(int(*)())
o più grande (fondamentalmente, essere della dimensione di un puntatore o più grande, come ci si aspetterebbe. std::function
Ha una dimensione minima implicita nello standard (devono essere in grado di memorizzare "dentro se stessi" chiamabili fino a una certa dimensione) che è grande almeno quanto un puntatore a funzione in pratica).
Nel int(*f)()
caso, stai memorizzando un puntatore a una funzione che si comporta come se chiamassi quel lambda. Funziona solo per lambda senza stato (quelli con un []
elenco di acquisizione vuoto ).
Nel std::function<int()> f
caso, stai creando std::function<int()>
un'istanza di classe di cancellazione del tipo che (in questo caso) utilizza il posizionamento new per archiviare una copia del lambda di dimensione 1 in un buffer interno (e, se è stato passato un lambda più grande (con più stato ), userebbe l'allocazione dell'heap).
Probabilmente qualcosa del genere è probabilmente quello che pensi stia succedendo. Che un lambda è un oggetto il cui tipo è descritto dalla sua firma. In C ++, è stato deciso di rendere lambda astrazioni a costo zero rispetto all'implementazione manuale dell'oggetto funzione. Questo ti consente di passare un lambda in un filestd
algoritmo (o simile) e di avere il suo contenuto completamente visibile al compilatore quando istanzia il modello di algoritmo. Se un lambda avesse un tipo simile std::function<void(int)>
, il suo contenuto non sarebbe completamente visibile e un oggetto funzione creato a mano potrebbe essere più veloce.
L'obiettivo della standardizzazione C ++ è la programmazione di alto livello con zero overhead rispetto al codice C realizzato a mano.
Ora che hai capito che il tuo f
è di fatto apolide, dovrebbe esserci un'altra domanda nella tua testa: la lambda non ha stato. Perché non ha le dimensioni 0
?
C'è la risposta breve.
Tutti gli oggetti in C ++ devono avere una dimensione minima di 1 in base allo standard e due oggetti dello stesso tipo non possono avere lo stesso indirizzo. Questi sono collegati, perché un array di tipo T
avrà gli elementi sizeof(T)
separati.
Ora, poiché non ha uno stato, a volte non può occupare spazio. Questo non può accadere quando è "da solo", ma in alcuni contesti può accadere. std::tuple
e un codice di libreria simile sfrutta questo fatto. Ecco come funziona:
Poiché un lambda è equivalente a una classe con operator()
sovraccarico, lambda senza stato (con un []
elenco di acquisizione) sono tutte classi vuote. Hanno sizeof
di 1
. In effetti, se erediti da loro (cosa consentita!), Non occuperanno spazio fintanto che non causano una collisione di indirizzi dello stesso tipo . (Questo è noto come l'ottimizzazione della base vuota).
template<class T>
struct toy:T {
toy(toy const&)=default;
toy(toy &&)=default;
toy(T const&t):T(t) {}
toy(T &&t):T(std::move(t)) {}
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
lo sizeof(make_toy( []{std::cout << "hello world!\n"; } ))
è sizeof(int)
(beh, quanto sopra è illegale perché non puoi creare un lambda in un contesto non valutato: devi creare un nome e auto toy = make_toy(blah);
poi farlo sizeof(blah)
, ma questo è solo rumore). sizeof([]{std::cout << "hello world!\n"; })
è ancora 1
(qualifiche simili).
Se creiamo un altro tipo di giocattolo:
template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):T(t), t2(t) {}
T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }
questo ha due copie del lambda. Poiché non possono condividere lo stesso indirizzo, sizeof(toy2(some_lambda))
è 2
!
struct
con anoperator()
)