Pourquoi un lambda a-t-il une taille de 1 octet?

89

Je travaille avec la mémoire de certains lambdas en C ++, mais je suis un peu perplexe par leur taille.

Voici mon code de test:

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

Vous pouvez l'exécuter ici: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

La sortie est:

17
0x7d90ba8f626f
1

Cela suggère que la taille de mon lambda est de 1.

  • Comment est-ce possible?

  • Le lambda ne devrait-il pas être, au minimum, un pointeur vers son implémentation?

sdgfsdh
la source
17
son implémenté en tant qu'objet de fonction (a structavec un operator())
george_ptr
14
Et une structure vide ne peut pas être de taille 0 d'où le résultat 1. Essayez de capturer quelque chose et voyez ce qu'il advient de la taille.
Mohamad Elghawi
2
Pourquoi un lambda devrait-il être un pointeur ??? C'est un objet qui a un opérateur d'appel.
Kerrek SB
7
Les lambdas en C ++ existent au moment de la compilation, et les appels sont liés (ou même intégrés) au moment de la compilation ou de la liaison. Il n'y a donc pas besoin d'un pointeur d' exécution dans l'objet lui-même. @KerrekSB Ce n'est pas une supposition artificielle de s'attendre à ce qu'un lambda contienne un pointeur de fonction, car la plupart des langages qui implémentent des lambdas sont plus dynamiques que C ++.
Kyle Strand
2
@KerrekSB "ce qui compte" - dans quel sens? La raison pour laquelle un objet de fermeture peut être vide (plutôt que de contenir un pointeur de fonction) est que la fonction à appeler est connue au moment de la compilation / liaison. C'est ce que le PO semble avoir mal compris. Je ne vois pas comment vos commentaires clarifient les choses.
Kyle Strand

Réponses:

107

Le lambda en question n'a en fait aucun état .

Examiner:

struct lambda {
  auto operator()() const { return 17; }
};

Et si nous en avions lambda f;, c'est une classe vide. Non seulement ce qui précède est lambdafonctionnellement similaire à votre lambda, mais c'est (essentiellement) comment votre lambda est implémenté! (Il a également besoin d'un opérateur de pointeur de conversion implicite en fonction, et le nom lambdava être remplacé par un pseudo-guid généré par le compilateur)

En C ++, les objets ne sont pas des pointeurs. Ce sont des choses réelles. Ils n'utilisent que l'espace nécessaire pour y stocker les données. Un pointeur vers un objet peut être plus grand qu'un objet.

Bien que vous puissiez considérer ce lambda comme un pointeur vers une fonction, ce n'est pas le cas. Vous ne pouvez pas réaffecter le auto f = [](){ return 17; };à une autre fonction ou lambda!

 auto f = [](){ return 17; };
 f = [](){ return -42; };

ce qui précède est illégal . Il n'y a pas de place dans fla boutique la fonction va être appelée - ces informations sont stockées dans le genre de f, pas la valeur f!

Si vous avez fait ceci:

int(*f)() = [](){ return 17; };

ou ca:

std::function<int()> f = [](){ return 17; };

vous ne stockez plus directement le lambda. Dans ces deux cas, f = [](){ return -42; }est légal - donc dans ces cas, nous stockons qui fonction nous invoquons la valeur de f. Et sizeof(f)n'est plus 1, mais plutôt sizeof(int(*)())ou plus grand (en gros, soyez de la taille du pointeur ou plus grand, comme vous vous en doutez. A std::functionune taille minimale impliquée par la norme (ils doivent pouvoir stocker des callables "en eux-mêmes" jusqu'à une certaine taille) qui est au moins aussi grand qu'un pointeur de fonction en pratique).

Dans ce int(*f)()cas, vous stockez un pointeur de fonction vers une fonction qui se comporte comme si vous appeliez ce lambda. Cela ne fonctionne que pour les lambdas sans état (ceux avec une []liste de capture vide ).

Dans ce std::function<int()> fcas, vous créez une std::function<int()>instance de classe d'effacement de type qui (dans ce cas) utilise le placement new pour stocker une copie du lambda de taille 1 dans un tampon interne (et, si un lambda plus grand a été passé (avec plus d'état ), utiliserait l'allocation de tas).

En guise de supposition, quelque chose comme celui-ci est probablement ce que vous pensez qu'il se passe. Qu'un lambda est un objet dont le type est décrit par sa signature. En C ++, il a été décidé de créer des abstractions lambdas à coût nul sur l'implémentation manuelle des objets de fonction. Cela vous permet de passer un lambda dans un stdalgorithme (ou similaire) et de faire en sorte que son contenu soit entièrement visible par le compilateur lorsqu'il instancie le modèle d'algorithme. Si un lambda avait un type comme std::function<void(int)>, son contenu ne serait pas entièrement visible et un objet fonction fabriqué à la main pourrait être plus rapide.

Le but de la standardisation C ++ est une programmation de haut niveau sans surcharge par rapport au code C fabriqué à la main.

Maintenant que vous comprenez que vous êtes fen fait apatride, il devrait y avoir une autre question dans votre tête: le lambda n'a pas d'état. Pourquoi n'a-t-il pas de taille 0?


Il y a la réponse courte.

Tous les objets en C ++ doivent avoir une taille minimale de 1 selon la norme, et deux objets du même type ne peuvent pas avoir la même adresse. Ceux-ci sont connectés, car un tableau de type Taura les éléments sizeof(T)séparés.

Maintenant, comme il n'a pas d'état, parfois il ne peut pas prendre de place. Cela ne peut pas arriver quand il est «seul», mais dans certains contextes, cela peut arriver. std::tupleet un code de bibliothèque similaire exploite ce fait. Voici comment cela fonctionne:

Comme un lambda équivaut à une classe operator()surchargée, les lambdas sans état (avec une []liste de capture) sont toutes des classes vides. Ils ont sizeofde 1. En fait, si vous en héritez (ce qui est autorisé!), Ils ne prendront pas de place tant que cela ne provoquera pas de collision d'adresse de même type . (Ceci est connu comme l'optimisation de la base vide).

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

le sizeof(make_toy( []{std::cout << "hello world!\n"; } ))est sizeof(int)(enfin, ce qui précède est illégal car vous ne pouvez pas créer un lambda dans un contexte non évalué: vous devez créer un named auto toy = make_toy(blah);then do sizeof(blah), mais ce n'est que du bruit). sizeof([]{std::cout << "hello world!\n"; })est toujours 1(qualifications similaires).

Si nous créons un autre type de jouet:

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

cela a deux copies du lambda. Comme ils ne peuvent pas partager la même adresse, sizeof(toy2(some_lambda))c'est 2!

Yakk - Adam Nevraumont
la source
6
Nit: un pointeur de fonction peut être plus petit qu'un vide *. Deux exemples historiques: Premièrement les machines adressées par mot où sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * et char * ont besoin de quelques bits supplémentaires pour contenir l'offset dans un mot) .Deuxièmement, le modèle de mémoire 8086 où void * / int * était segment + offset et pouvait couvrir toute la mémoire, mais les fonctions ajustées dans un seul segment de 64K ( donc un pointeur de fonction était seulement 16 bits).
Martin Bonner soutient Monica
1
@martin vrai. Extra ()ajouté.
Yakk - Adam Nevraumont
50

Un lambda n'est pas un pointeur de fonction.

Un lambda est une instance d'une classe. Votre code équivaut approximativement à:

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 interne qui représente un lambda n'a pas de membres de classe, d'où sa valeur sizeof()1 (elle ne peut pas être 0, pour des raisons correctement exposées ailleurs ).

Si votre lambda devait capturer certaines variables, elles seront équivalentes aux membres de la classe, et vous sizeof()l'indiquerez en conséquence.

Sam Varshavchik
la source
3
Pourriez-vous créer un lien vers «ailleurs», ce qui explique pourquoi le sizeof()ne peut pas être 0?
user1717828
26

Votre compilateur traduit plus ou moins le lambda en type struct suivant:

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

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

Étant donné que cette structure n'a pas de membres non statiques, elle a la même taille qu'une structure vide, ce qui est 1.

Cela change dès que vous ajoutez une liste de capture non vide à votre lambda:

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

Ce qui se traduira par

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

Étant donné que la structure générée doit maintenant stocker un intmembre non statique pour la capture, sa taille augmentera jusqu'à sizeof(int). La taille continuera d'augmenter au fur et à mesure que vous capturerez plus de choses.

(Veuillez prendre l'analogie de la structure avec un grain de sel. Bien que ce soit une bonne façon de raisonner sur le fonctionnement interne des lambdas, ce n'est pas une traduction littérale de ce que le compilateur fera)

ComicSansMS
la source
12

Le lambda ne devrait-il pas être, au minimum, un pointeur vers son implémentation?

Pas nécessairement. Selon la norme, la taille de la classe unique et sans nom est définie par l'implémentation . Extrait de [expr.prim.lambda] , C ++ 14 (c'est moi qui souligne):

Le type de l'expression lambda (qui est également le type de l'objet de fermeture) est un type de classe non union unique et sans nom - appelé type de fermeture - dont les propriétés sont décrites ci-dessous.

[...]

Une implémentation peut définir le type de fermeture différemment de ce qui est décrit ci-dessous à condition que cela ne modifie pas le comportement observable du programme autrement qu'en modifiant :

- la taille et / ou l'alignement du type de fermeture ,

- si le type de fermeture est trivialement copiable (Article 9),

- si le type de fermeture est une classe de mise en page standard (Article 9), ou

- si le type de fermeture est une classe POD (Article 9)

Dans votre cas - pour le compilateur que vous utilisez - vous obtenez une taille de 1, ce qui ne signifie pas que c'est corrigé. Il peut varier entre les différentes implémentations du compilateur.

légendes2k
la source
Êtes-vous sûr que ce bit s'applique? Un lambda sans groupe de capture n'est pas vraiment une «fermeture». (La norme fait-elle référence aux lambdas de groupe de capture vide comme des «fermetures» de toute façon?)
Kyle Strand
1
Oui. C'est ce que dit le standard " L'évaluation d'une expression lambda aboutit à une prvalue temporaire. Ce temporaire s'appelle l'objet de fermeture. "
legends2k
Je n'ai pas voté contre, mais peut-être que le contrevenant ne pense pas que cette réponse soit valable car elle n'explique pas pourquoi il est possible (d'un point de vue théorique et non standard) d'implémenter des lambdas sans inclure un pointeur d'exécution vers le fonction d'appel d'opérateur. (Voir ma discussion avec KerrekSB sous la question.)
Kyle Strand
7

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

L'expression lambda construit un objet temporaire prvalue sans nom d'un type de classe non-agrégé non-union unique sans nom, connu sous le nom de type de fermeture , qui est déclaré (aux fins d'ADL) dans la plus petite portée de bloc, la portée de classe ou la portée d'espace de noms qui contient l'expression lambda.

Si l'expression lambda capture quoi que ce soit par copie (soit implicitement avec la clause de capture [=], soit explicitement avec une capture qui n'inclut pas le caractère &, par exemple [a, b, c]), le type de fermeture inclut des données non statiques sans nom membres , déclarés dans un ordre non spécifié, qui détiennent des copies de toutes les entités ainsi capturées.

Pour les entités qui sont capturées par référence (avec la capture par défaut [&] ou lors de l'utilisation du caractère &, par exemple [& a, & b, & c]), il n'est pas spécifié si des membres de données supplémentaires sont déclarés dans le type de fermeture

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

Lorsqu'il est appliqué à un type de classe vide, renvoie toujours 1.

george_ptr
la source