web-dev-qa-db-fra.com

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

Je travaille avec la mémoire de quelques 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

Le ouptut 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, au minimum, être un pointeur vers sa mise en œuvre?

89
sdgfsdh

Le lambda en question a en fait pas d'état.

Examiner:

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

Et si nous avions lambda f;, C'est une classe vide. Non seulement le lambda ci-dessus est fonctionnellement similaire à votre lambda, mais c'est (fondamentalement) comment votre lambda est implémenté! (Il a également besoin d'un transtypage implicite pour fonctionner comme un opérateur de pointeur, et le nom lambda va être remplacé par un pseudo-guide 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 requis pour y stocker les données. Un pointeur sur un objet peut être plus grand qu'un objet.

Bien que vous puissiez penser à ce lambda comme un pointeur vers une fonction, ce n'est pas le cas. Vous ne pouvez pas réaffecter la 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 f pour stocker laquelle la fonction va être appelée - cette information est stockée dans le type de f , pas dans la valeur de 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 laquelle fonction que nous invoquons dans la valeur de f. Et sizeof(f) n'est plus 1, Mais plutôt sizeof(int(*)()) ou plus (en gros, être de la taille d'un pointeur ou plus, comme vous vous y attendez. std::function A un taille minimale impliquée par la norme (ils doivent être capables de stocker des callables "en eux-mêmes" jusqu'à une certaine taille) qui est au moins aussi grande qu'un pointeur de fonction en pratique).

Dans le cas int(*f)(), vous stockez un pointeur de fonction sur 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 le cas std::function<int()> f, vous créez une instance de classe d'effacement de type std::function<int()> qui (dans ce cas) utilise placement new pour stocker une copie du lambda de taille 1 dans un tampon interne (et, si un lambda plus grand était passé (avec plus d'état), utiliserait l'allocation de tas).

À titre de supposition, quelque chose comme ça est probablement ce que vous pensez qui 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 faire des lambdas zéro coût abstractions sur l'implémentation manuelle de l'objet fonction. Cela vous permet de passer un lambda dans un algorithme std (ou similaire) et d'avoir son contenu entièrement visible pour 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 normalisation C++ est la programmation de haut niveau sans frais généraux par rapport au code C fabriqué à la main.

Maintenant que vous comprenez que votre f est en fait apatride, il devrait y avoir une autre question dans votre tête: la lambda n'a pas d'état. Pourquoi la taille n'a-t-elle pas 0?


Voilà la réponse courte.

Tous les objets en C++ doivent avoir une taille minimale de 1 sous 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 T aura les éléments placés sizeof(T) à part.

Maintenant, comme il n'a pas d'état, il peut parfois ne pas prendre de place. Cela ne peut pas se produire lorsqu'il est "seul", mais dans certains contextes, cela peut se produire. std::Tuple Et un code de bibliothèque similaire exploite ce fait. Voici comment cela fonctionne:

Comme un lambda est équivalent à une classe avec operator() surchargée, les lambdas sans état (avec une liste de capture []) Sont toutes des classes vides. Ils ont sizeof de 1. En fait, si vous en héritez (ce qui est autorisé!), Ils ne prendront aucun espace tant que cela ne provoquera pas une 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}; }

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 auto toy = make_toy(blah); nommé puis faire sizeof(blah), mais c'est juste 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)) est 2!

107

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, donc sa sizeof() est 1 (elle ne peut pas être 0, pour des raisons correctement indiquées ailleurs ).

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

50
Sam Varshavchik

Votre compilateur traduit plus ou moins le lambda dans le type de structure suivant:

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

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

Comme cette structure n'a pas de membres non statiques, elle a la même taille qu'une structure vide, 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 membre int non statique pour la capture, sa taille passera à sizeof(int). La taille ne cessera de croître à mesure que vous capturez plus de choses.

(Veuillez prendre l'analogie 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)

26
ComicSansMS

Le lambda ne devrait-il pas être, au minimum, un indicateur de sa mise en œuvre?

Pas nécessairement. Selon la norme, la taille de la classe unique 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é le type de fermeture - dont les propriétés sont décrites ci-dessous.

[...]

ne 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 changeant:

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

- si le type de fermeture est facilement copiable (article 9),

- si le type de fermeture est une classe à disposition 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 qu'il est fixe. Il peut varier entre différentes implémentations du compilateur.

12
legends2k

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

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

Si l'expression lambda capture quelque chose 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 membres de données non statiques non nommés , déclarés dans un ordre non spécifié, qui contiennent des copies de toutes les entités qui ont été ainsi capturés.

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 est non spécifié si des membres de données supplémentaires sont déclarés dans le type de fermeture

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

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

7
george_ptr