Por que um lambda tem o tamanho de 1 byte?

89

Estou trabalhando com a memória de alguns lambdas em C ++, mas estou um pouco confuso com o tamanho deles.

Aqui está meu código de teste:

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

Você pode executá-lo aqui: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

A saída é:

17
0x7d90ba8f626f
1

Isso sugere que o tamanho do meu lambda é 1.

  • Como isso é possível?

  • O lambda não deveria ser, no mínimo, um ponteiro para sua implementação?

sdgfsdh
fonte
17
é implementado como um objeto de função (a structcom um operator())
george_ptr
14
E uma estrutura vazia não pode ter tamanho 0, portanto, o resultado 1. Tente capturar algo e veja o que acontece com o tamanho.
Mohamad Elghawi
2
Por que um lambda deve ser um ponteiro ??? É um objeto que possui uma operadora de chamada.
Kerrek SB
7
Lambdas em C ++ existem em tempo de compilação e as invocações são vinculadas (ou mesmo embutidas) em tempo de compilação ou link. Portanto, não há necessidade de um ponteiro de tempo de execução no próprio objeto. @KerrekSB Não é uma suposição anormal esperar que um lambda contenha um ponteiro de função, uma vez que a maioria das linguagens que implementam lambdas são mais dinâmicas que C ++.
Kyle Strand
2
@KerrekSB "o que importa" - em que sentido? O motivo pelo qual um objeto de encerramento pode estar vazio (em vez de conter um ponteiro de função) é porque a função a ser chamada é conhecida em tempo de compilação / link. Isso é o que o OP parece não ter entendido. Não vejo como seus comentários esclarecem as coisas.
Kyle Strand

Respostas:

107

O lambda em questão, na verdade, não tem estado .

Examinar:

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

E se tivéssemos lambda f;, é uma classe vazia. Não só o anterior é lambdafuncionalmente semelhante ao seu lambda, mas (basicamente) como o seu lambda é implementado! (Ele também precisa de uma conversão implícita para o operador de ponteiro de função, e o nome lambdaserá substituído por algum pseudo-guid gerado pelo compilador)

Em C ++, os objetos não são ponteiros. Eles são coisas reais. Eles só usam o espaço necessário para armazenar os dados neles. Um ponteiro para um objeto pode ser maior do que um objeto.

Embora você possa pensar nesse lambda como um ponteiro para uma função, não é. Você não pode reatribuir o auto f = [](){ return 17; };a uma função diferente ou lambda!

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

o acima é ilegal . Não há espaço fpara armazenar qual função será chamada - as informações são armazenadas no tipo de f, não no valor de f!

Se você fez isso:

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

ou isto:

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

você não está mais armazenando o lambda diretamente. Em ambos os casos, f = [](){ return -42; }é legal - por isso, nestes casos, estamos armazenando qual função estamos invocando no valor f. E sizeof(f)não é mais 1, mas sim sizeof(int(*)())ou maior (basicamente, seja do tamanho de um ponteiro ou maior, como você espera. std::functionTem um tamanho mínimo implícito pelo padrão (eles devem ser capazes de armazenar "dentro de si" chamáveis ​​até um certo tamanho) que é pelo menos tão grande quanto um ponteiro de função na prática).

Nesse int(*f)()caso, você está armazenando um ponteiro de função para uma função que se comporta como se você chamasse aquele lambda. Isso só funciona para lambdas sem estado (aqueles com uma []lista de captura vazia ).

No std::function<int()> fcaso, você está criando uma std::function<int()>instância de classe de eliminação de tipo que (neste caso) usa a colocação new para armazenar uma cópia do lambda de tamanho 1 em um buffer interno (e, se um lambda maior foi passado (com mais estado ), usaria alocação de heap).

Como um palpite, algo assim é provavelmente o que você acha que está acontecendo. Que um lambda é um objeto cujo tipo é descrito por sua assinatura. Em C ++, foi decidido fazer lambdas abstrações de custo zero sobre a implementação manual do objeto de função. Isso permite que você passe um lambda para um stdalgoritmo (ou semelhante) e tenha seu conteúdo totalmente visível para o compilador quando ele instancia o modelo do algoritmo. Se um lambda tivesse um tipo como std::function<void(int)>, seu conteúdo não seria totalmente visível e um objeto de função feito à mão poderia ser mais rápido.

O objetivo da padronização do C ++ é a programação de alto nível com zero overhead em relação ao código C feito à mão.

Agora que você entende que, fna verdade, você não tem estado, deve haver outra pergunta em sua cabeça: o lambda não tem estado. Por que não tem tamanho 0?


Essa é a resposta curta.

Todos os objetos em C ++ devem ter um tamanho mínimo de 1 sob o padrão e dois objetos do mesmo tipo não podem ter o mesmo endereço. Eles estão conectados, porque um array do tipo Tterá os elementos sizeof(T)separados.

Agora, como não tem estado, às vezes não pode ocupar espaço. Isso não pode acontecer quando está "sozinho", mas em alguns contextos pode acontecer. std::tuplee um código de biblioteca semelhante explora esse fato. É assim que funciona:

Como um lambda é equivalente a uma classe com operator()sobrecarregados, lambdas sem estado (com uma []lista de captura) são classes vazias. Eles têm sizeofde 1. Na verdade, se você herdar deles (o que é permitido!), Eles não ocuparão espaço , desde que não causem uma colisão de endereços do mesmo tipo . (Isso é conhecido como otimização de base vazia).

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

o sizeof(make_toy( []{std::cout << "hello world!\n"; } ))is sizeof(int)(bem, o acima é ilegal porque você não pode criar um lambda em um contexto não avaliado: você precisa criar um nomeado e auto toy = make_toy(blah);depois fazer sizeof(blah), mas isso é apenas ruído). sizeof([]{std::cout << "hello world!\n"; })ainda é 1(qualificações semelhantes).

Se criarmos outro tipo de brinquedo:

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

isso tem duas cópias do lambda. Como não podem compartilhar o mesmo endereço, sizeof(toy2(some_lambda))é 2!

Yakk - Adam Nevraumont
fonte
6
Nit: Um ponteiro de função pode ser menor que um vazio *. Dois exemplos históricos: Primeiramente, máquinas endereçadas a palavras onde sizeof (void *) == sizeof (char *)> sizeof (struct *) == sizeof (int *). (void * e char * precisam de alguns bits extras para manter o deslocamento dentro de uma palavra). Em segundo lugar, o modelo de memória 8086 onde void * / int * era segmento + deslocamento e poderia cobrir toda a memória, mas as funções cabiam em um único segmento de 64K ( portanto, um ponteiro de função tinha apenas 16 bits).
Martin Bonner apoia Monica
1
@martin true. Extra ()adicionado.
Yakk - Adam Nevraumont
50

Um lambda não é um ponteiro de função.

Um lambda é uma instância de uma classe. Seu código é aproximadamente 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;

A classe interna que representa um lambda não tem membros de classe, portanto, sizeof()é 1 (não pode ser 0, por razões adequadamente declaradas em outro lugar ).

Se seu lambda capturar algumas variáveis, elas serão equivalentes aos membros da classe e você sizeof()indicará de acordo.

Sam Varshavchik
fonte
3
Você poderia vincular a "outro lugar", o que explica por que o sizeof()não pode ser 0?
user1717828
26

Seu compilador traduz mais ou menos o lambda para o seguinte tipo de estrutura:

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

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

Como essa estrutura não tem membros não estáticos, ela tem o mesmo tamanho de uma estrutura vazia, que é 1.

Isso muda assim que você adiciona uma lista de captura não vazia ao seu lambda:

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

Que irá traduzir para

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

Como a estrutura gerada agora precisa armazenar um intmembro não estático para a captura, seu tamanho aumentará para sizeof(int). O tamanho continuará crescendo conforme você captura mais coisas.

(Por favor, considere a analogia da estrutura com um grão de sal. Embora seja uma boa maneira de raciocinar sobre como os lambdas funcionam internamente, esta não é uma tradução literal do que o compilador fará)

ComicSansMS
fonte
12

O lambda não deveria ser, no mínimo, um ponteiro para sua implementação?

Não necessariamente. De acordo com o padrão, o tamanho da classe única e sem nome é definido pela implementação . Trecho de [expr.prim.lambda] , C ++ 14 (ênfase minha):

O tipo da expressão lambda (que também é o tipo do objeto de fechamento) é um tipo de classe sem união sem nome exclusivo - chamado de tipo de fechamento - cujas propriedades são descritas abaixo.

[...]

Uma implementação pode definir o tipo de fechamento de forma diferente do que é descrito abaixo, desde que isso não altere o comportamento observável do programa, exceto pela alteração :

- o tamanho e / ou alinhamento do tipo de fechamento ,

- se o tipo de fechamento é trivialmente copiável (Cláusula 9),

- se o tipo de fechamento é uma classe de layout padrão (Cláusula 9), ou

- se o tipo de fechamento é uma classe POD (Cláusula 9)

No seu caso - para o compilador que você usa - você obtém o tamanho 1, o que não significa que seja fixo. Ele pode variar entre as diferentes implementações do compilador.

legends2k
fonte
Tem certeza que esta parte se aplica? Um lambda sem um grupo de captura não é realmente um "encerramento". (O padrão se refere a lambdas de grupo de captura vazio como "fechamentos" de qualquer maneira?)
Kyle Strand
1
Sim. Isso é o que o padrão diz " A avaliação de uma expressão lambda resulta em um temporário prvalue. Este temporário é chamado de objeto de fechamento. ", Capturando ou não, é um objeto de fechamento, só que não terá upvalues.
legends2k
Eu não fiz downvote, mas possivelmente o downvoter não acha que esta resposta é valiosa porque não explica por que é possível (de uma perspectiva teórica, não uma perspectiva de padrões) implementar lambdas sem incluir um ponteiro de tempo de execução para o função de operador de chamada. (Veja minha discussão com KerrekSB na pergunta.)
Kyle Strand
7

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

A expressão lambda constrói um objeto temporário prvalue sem nome de um tipo de classe não agregado sem união sem nome exclusivo, conhecido como tipo de fechamento , que é declarado (para fins de ADL) no menor escopo de bloco, escopo de classe ou escopo de namespace que contém a expressão lambda.

Se a expressão lambda capturar qualquer coisa por cópia (implicitamente com a cláusula de captura [=] ou explicitamente com uma captura que não inclui o caractere &, por exemplo, [a, b, c]), o tipo de fechamento inclui dados não estáticos não nomeados membros , declarados em ordem não especificada, que mantêm cópias de todas as entidades assim capturadas.

Para as entidades que são capturadas por referência (com a captura padrão [&] ou ao usar o caractere &, por exemplo, [& a, & b, & c]), não é especificado se membros de dados adicionais são declarados no tipo de fechamento

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

Quando aplicado a um tipo de classe vazio, sempre retorna 1.

george_ptr
fonte