De acordo com as fontes que encontrei, uma expressão lambda é essencialmente implementada pelo compilador, criando uma classe com o operador de chamada de função sobrecarregado e as variáveis referenciadas como membros. Isso sugere que o tamanho das expressões lambda varia e, dadas variáveis de referências suficientes, esse tamanho pode ser arbitrariamente grande .
Um std::function
deve ter um tamanho fixo , mas deve ser capaz de encapsular qualquer tipo de callables, incluindo quaisquer lambdas do mesmo tipo. Como isso é implementado? Se std::function
usar internamente um ponteiro para seu destino, o que acontecerá quando a std::function
instância for copiada ou movida? Há alguma alocação de heap envolvida?
std::function
um tempo atrás. É essencialmente uma classe de identificador para um objeto polimórfico. Uma classe derivada da classe base interna é criada para conter os parâmetros, alocados no heap - então o ponteiro para isso é mantido como um subobjeto destd::function
. Eu acredito que ele usa a contagem de referênciastd::shared_ptr
para lidar com a cópia e movimentação.Respostas:
A implementação de
std::function
pode ser diferente de uma implementação para outra, mas a ideia central é que ele usa apagamento de tipo. Embora existam várias maneiras de fazer isso, você pode imaginar uma solução trivial (não ideal) como esta (simplificada para o caso específico destd::function<int (double)>
por uma questão de simplicidade):Nesta abordagem simples, o
function
objeto armazenaria apenas umunique_ptr
em um tipo base. Para cada functor diferente usado com ofunction
, um novo tipo derivado da base é criado e um objeto desse tipo instanciado dinamicamente. Ostd::function
objeto é sempre do mesmo tamanho e alocará espaço conforme necessário para os diferentes functores no heap.Na vida real, existem diferentes otimizações que fornecem vantagens de desempenho, mas complicariam a resposta. O tipo pode usar otimizações de pequenos objetos, o despacho dinâmico pode ser substituído por um ponteiro de função livre que leva o functor como argumento para evitar um nível de indireção ... mas a ideia é basicamente a mesma.
Em relação à questão de como as cópias do
std::function
se comportam, um teste rápido indica que as cópias do objeto chamável interno são feitas, em vez de compartilhar o estado.O teste indica que
f2
obtém uma cópia da entidade que pode ser chamada, em vez de uma referência. Se a entidade chamável fosse compartilhada porstd::function<>
objetos diferentes , a saída do programa teria sido 5, 6, 7.fonte
std::function
estaria correta se o objeto interno fosse copiado, e eu não acho que seja o caso (pense em um lambda que captura um valor e é mutável, armazenado dentro de umstd::function
, se o estado da função foi copiado o número de cópias destd::function
dentro de um algoritmo padrão poderia resultar em resultados diferentes, o que é indesejável.std::function
acionará uma alocação.A resposta de @David Rodríguez - dribeas é bom para demonstrar o apagamento de tipo, mas não o suficiente, já que o apagamento de tipo também inclui como os tipos são copiados (nessa resposta o objeto de função não pode ser copiado). Esses comportamentos também são armazenados no
function
objeto, além dos dados do functor.O truque, usado na implementação STL do Ubuntu 14.04 gcc 4.8, é escrever uma função genérica, especializá-la com cada tipo de função possível e convertê-los em um tipo de ponteiro de função universal. Portanto, as informações de tipo são apagadas .
Eu criei uma versão simplificada disso. Espero que ajude
Existem também algumas otimizações na versão STL
construct_f
edestroy_f
são misturados em um ponteiro de função (com um parâmetro adicional que diz o que fazer) para salvar alguns bytesunion
, de modo que quando umfunction
objeto é construído a partir de um ponteiro de função, ele será armazenado diretamente nounion
espaço ao invés de heapTalvez a implementação de STL não seja a melhor solução, já que ouvi falar de algumas implementações mais rápidas . No entanto, acredito que o mecanismo subjacente é o mesmo.
fonte
Para certos tipos de argumentos ("se o alvo de f é um objeto que pode ser chamado passado por
reference_wrapper
ou um ponteiro de função"),std::function
o construtor de não permite quaisquer exceções, portanto, usar memória dinâmica está fora de questão. Para este caso, todos os dados devem ser armazenados diretamente dentro dostd::function
objeto.No caso geral, (incluindo o caso lambda), o uso de memória dinâmica (por meio do alocador padrão ou de um alocador passado para o
std::function
construtor) é permitido conforme a implementação considerar adequada. O padrão recomenda que as implementações não usem memória dinâmica se puder ser evitado, mas como você diz com razão, se o objeto de função (não ostd::function
objeto, mas o objeto sendo envolvido nele) for grande o suficiente, não há como evitá-lo, poisstd::function
tem um tamanho fixo.Essa permissão para lançar exceções é concedida ao construtor normal e ao construtor de cópia, o que também permite alocações de memória dinâmica durante a cópia de forma bastante explícita. Para movimentos, não há razão para que a memória dinâmica seja necessária. O padrão não parece proibi-lo explicitamente, e provavelmente não pode se o movimento pode chamar o construtor de movimento do tipo do objeto empacotado, mas você deve ser capaz de assumir que, se a implementação e seus objetos forem sensíveis, o movimento não causará quaisquer alocações.
fonte
Um
std::function
sobrecarrega ooperator()
tornando um objeto functor, o lambda funciona da mesma maneira. Basicamente, ele cria uma estrutura com variáveis de membro que podem ser acessadas dentro daoperator()
função. Portanto, o conceito básico a ter em mente é que lambda é um objeto (chamado de functor ou objeto de função), não uma função. O padrão diz para não usar memória dinâmica se puder ser evitada.fonte
std::function
? Essa é a questão chave aqui.std::function
objetos têm o mesmo tamanho e não são do tamanho dos lambdas contidos.std::vector<T...>
objeto tem um tamanho fixo (copilime) independente da instância real do alocador / número de elementos.std::function<void ()> f;
não há necessidade de alocar lá,std::function<void ()> f = [&]() { /* captures tons of variables */ };
provavelmente aloca.std::function<void()> f = &free_function;
provavelmente também não aloca ...