Perché un lambda ha una dimensione di 1 byte?


89

Sto lavorando con la memoria di alcuni lambda in C ++, ma sono un po 'perplesso dalla loro dimensione.

Ecco il mio codice di prova:

#include <iostream>
#include <string>

int main()
{
  auto f = [](){ return 17; };
  std::cout << f() << std::endl;
  std::cout << &f << std::endl;
  std::cout << sizeof(f) << std::endl;
}

Puoi eseguirlo qui: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

L'uscita è:

17
0x7d90ba8f626f
1

Ciò suggerisce che la dimensione del mio lambda è 1.

  • Com'è possibile?

  • Il lambda non dovrebbe essere, come minimo, un puntatore alla sua implementazione?


17
è implementato come oggetto funzione (una structcon an operator())
george_ptr

14
E una struttura vuota non può essere di dimensione 0, quindi il risultato 1. Prova a catturare qualcosa e guarda cosa succede alle dimensioni.
Mohamad Elghawi

2
Perché un lambda dovrebbe essere un puntatore ??? È un oggetto che ha un operatore di chiamata.
Kerrek SB

7
Lambda in C ++ esistono in fase di compilazione e le invocazioni sono collegate (o anche inline) in fase di compilazione o collegamento. Non è quindi necessario un puntatore a runtime nell'oggetto stesso. @KerrekSB Non è un'ipotesi innaturale aspettarsi che un lambda contenga un puntatore a funzione, poiché la maggior parte dei linguaggi che implementano lambda sono più dinamici del C ++.
Kyle Strand

2
@KerrekSB "ciò che conta" - in che senso? Il motivo per cui un oggetto di chiusura può essere vuoto (piuttosto che contenere un puntatore a funzione) è perché la funzione da chiamare è nota in fase di compilazione / collegamento. Questo è ciò che l'OP sembra aver frainteso. Non vedo come i tuoi commenti chiariscano le cose.
Kyle Strand

Risposte:


107

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 è lambdafunzionalmente 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 lambdaverrà 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 fper 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::functionHa 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 Tavrà 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::tuplee 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 sizeofdi 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!


6
Nit: un puntatore a funzione può essere più piccolo di un vuoto *. Due esempi storici: in primo luogo macchine indirizzate a parole dove sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * e char * ha bisogno di alcuni bit extra per contenere l'offset all'interno di una parola) .In secondo luogo il modello di memoria 8086 dove void * / int * era segmento + offset e poteva coprire tutta la memoria, ma le funzioni sono state inserite in un singolo segmento da 64K ( quindi un puntatore a funzione era a soli 16 bit).
Martin Bonner sostiene Monica

1
@martin vero. ()Aggiunto extra .
Yakk - Adam Nevraumont

50

Un lambda non è un puntatore a funzione.

Un lambda è un'istanza di una classe. Il tuo codice è approssimativamente equivalente a:

class f_lambda {
public:

  auto operator() { return 17; }
};

f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;

La classe interna che rappresenta un lambda non ha membri di classe, quindi sizeof()è 1 (non può essere 0, per ragioni adeguatamente indicate altrove ).

Se il tuo lambda dovesse catturare alcune variabili, saranno equivalenti ai membri della classe e il tuo sizeof()indicherà di conseguenza.


3
Potresti collegarti a "altrove", il che spiega perché sizeof()non può essere 0?
user1717828

26

Il tuo compilatore traduce più o meno il lambda nel seguente tipo di struttura:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}

Poiché quella struttura non ha membri non statici, ha la stessa dimensione di una struttura vuota, che è 1.

Ciò cambia non appena aggiungi un elenco di acquisizione non vuoto al tuo lambda:

int i = 42;
auto f = [i]() { return i; };

Che si tradurrà in

struct _SomeInternalName {
    int i;
    _SomeInternalName(int outer_i) : i(outer_i) {}
    int operator()() { return i; }
};


int main()
{
     int i = 42;
     _SomeInternalName f(i);
     std::cout << f() << std::endl;
}

Poiché la struttura generata ora deve memorizzare un intmembro non statico per l'acquisizione, le sue dimensioni aumenteranno sizeof(int). Le dimensioni continueranno a crescere man mano che acquisisci più cose.

(Si prega di prendere l'analogia della struttura con un pizzico di sale. Sebbene sia un bel modo per ragionare su come funzionano internamente i lambda, questa non è una traduzione letterale di ciò che farà il compilatore)


12

Il lambda non dovrebbe essere, al minimo, un puntatore alla sua implementazione?

Non necessariamente. Secondo lo standard, la dimensione della classe univoca e senza nome è definita dall'implementazione . Estratto da [expr.prim.lambda] , C ++ 14 (enfasi mia):

Il tipo di espressione lambda (che è anche il tipo di oggetto di chiusura) è un tipo di classe non union univoco e senza nome, denominato tipo di chiusura, le cui proprietà sono descritte di seguito.

[...]

Un'implementazione può definire il tipo di chiusura in modo diverso da quanto descritto di seguito a condizione che ciò non alteri il comportamento osservabile del programma se non cambiando :

- la dimensione e / o l'allineamento del tipo di chiusura ,

- se il tipo di chiusura è banalmente copiabile (clausola 9),

- se il tipo di chiusura è una classe di layout standard (clausola 9), o

- se il tipo di chiusura è una classe POD (clausola 9)

Nel tuo caso, per il compilatore che usi, ottieni una dimensione di 1, il che non significa che sia corretto. Può variare tra le diverse implementazioni del compilatore.


Sei sicuro che questo bit si applichi? Un lambda senza un gruppo di cattura non è realmente una "chiusura". (Lo standard si riferisce comunque a lambda di gruppi di cattura vuoti come "chiusure"?)
Kyle Strand,

1
Sì, lo fa. Questo è ciò che dice lo standard " La valutazione di un'espressione lambda si traduce in un prvalue temporaneo. Questo temporaneo è chiamato oggetto di chiusura. ", Cattura o meno, è un oggetto di chiusura, solo quello sarà privo di upvalues.
legends2k

Non ho votato in negativo, ma forse il votante non pensa che questa risposta sia preziosa perché non spiega perché è possibile (da una prospettiva teorica, non da una prospettiva standard) implementare lambda senza includere un puntatore di runtime al funzione operatore di chiamata. (Vedi la mia discussione con KerrekSB sotto la domanda.)
Kyle Strand

7

Da http://en.cppreference.com/w/cpp/language/lambda :

L'espressione lambda costruisce un oggetto temporaneo prvalue senza nome di un tipo di classe non aggregato non-unione senza nome univoco, noto come tipo di chiusura , che è dichiarato (ai fini di ADL) nel più piccolo ambito di blocco, ambito di classe o spazio dei nomi che contiene l'espressione lambda.

Se l'espressione lambda acquisisce qualcosa per copia (sia implicitamente con la clausola di cattura [=] o esplicitamente con una cattura che non include il carattere &, ad esempio [a, b, c]), il tipo di chiusura include dati non statici senza nome membri , dichiarati in ordine non specificato, che detengono copie di tutte le entità che sono state così catturate.

Per le entità catturate per riferimento (con l'acquisizione predefinita [&] o quando si utilizza il carattere &, ad esempio [& a, & b, & c]), non è specificato se nel tipo di chiusura vengono dichiarati membri di dati aggiuntivi

Da http://en.cppreference.com/w/cpp/language/sizeof

Quando applicato a un tipo di classe vuoto, restituisce sempre 1.

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.