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 é lambda
funcionalmente 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 lambda
será 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 f
para 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::function
Tem 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()> f
caso, 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 std
algoritmo (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, f
na 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 T
terá 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::tuple
e 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 sizeof
de 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
!
struct
com umoperator()
)