Devo usar std :: function ou um ponteiro de função em C ++?

141

Ao implementar uma função de retorno de chamada em C ++, ainda devo usar o ponteiro de função no estilo C:

void (*callbackFunc)(int);

Ou devo usar std :: function:

std::function< void(int) > callbackFunc;
Jan Swart
fonte
9
Se a função de retorno de chamada for conhecida no momento da compilação, considere um modelo.
Baum mit Augen
4
Ao implementar uma função de retorno de chamada, você deve fazer o que o chamador exigir. Se sua pergunta é realmente sobre o design de uma interface de retorno de chamada, não há informações suficientes aqui para respondê-la. O que você deseja que o destinatário do seu retorno de chamada faça? Quais informações você precisa passar para o destinatário? Quais informações o destinatário deve retornar para você como resultado da chamada?
Pete Becker

Respostas:

171

Em resumo, use astd::function menos que você tenha um motivo para não fazê-lo.

Os ponteiros de função têm a desvantagem de não conseguir capturar algum contexto. Você não poderá, por exemplo, transmitir uma função lambda como um retorno de chamada que captura algumas variáveis ​​de contexto (mas funcionará se não capturar nenhuma). Chamar uma variável membro de um objeto (ou seja, não estático) também não é possível, pois o objeto ( this-pointer) precisa ser capturado. (1)

std::function(desde C ++ 11) é principalmente para armazenar uma função (distribuí-la não requer que ela seja armazenada). Portanto, se você deseja armazenar o retorno de chamada, por exemplo, em uma variável de membro, provavelmente é sua melhor escolha. Mas também se você não armazená-lo, é uma boa "primeira escolha", embora tenha a desvantagem de introduzir uma sobrecarga (muito pequena) ao ser chamada (portanto, em uma situação muito crítica de desempenho, pode ser um problema, mas na maioria Não deveria). É muito "universal": se você se preocupa muito com códigos consistentes e legíveis e não quer pensar em todas as escolhas que você faz (por exemplo, quer simplificar), use std::functionpara todas as funções que passar.

Pense em uma terceira opção: se você está prestes a implementar uma função pequena que relata algo por meio da função de retorno de chamada fornecida, considere um parâmetro de modelo , que pode ser qualquer objeto que possa ser chamado , como ponteiro de função, functor, lambda, a std::function, ... A desvantagem aqui é que sua função (externa) se torna um modelo e, portanto, precisa ser implementada no cabeçalho. Por outro lado, você obtém a vantagem de que a chamada para o retorno de chamada pode ser incorporada, pois o código do cliente da sua função (externa) "vê" a chamada para o retorno de chamada com as informações exatas do tipo disponíveis.

Exemplo para a versão com o parâmetro template (escreva em &vez de &&pré-C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Como você pode ver na tabela a seguir, todos eles têm suas vantagens e desvantagens:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) Existem soluções alternativas para superar essa limitação, por exemplo, passando os dados adicionais como parâmetros adicionais para sua função (externa): myFunction(..., callback, data)será chamada callback(data). Esse é o "retorno de chamada com argumentos" no estilo C, que é possível em C ++ (e a propósito muito usado na API do WIN32), mas deve ser evitado porque temos melhores opções em C ++.

(2) A menos que estejamos falando sobre um modelo de classe, ou seja, a classe na qual você armazena a função é um modelo. Mas isso significa que, no lado do cliente, o tipo da função decide o tipo do objeto que armazena o retorno de chamada, o que quase nunca é uma opção para casos de uso reais.

(3) Para pré-C ++ 11, use boost::function

leemes
fonte
9
ponteiros de função têm sobrecarga de chamada em comparação com os parâmetros do modelo. Os parâmetros do modelo facilitam o inlining, mesmo se você passar dez níveis acima, porque o código que está sendo executado é descrito pelo tipo de parâmetro e não pelo valor. E os objetos de função de modelo que estão sendo armazenados nos tipos de retorno de modelo são um padrão comum e útil (com um bom construtor de cópias, você pode criar a função de modelo eficiente invocável que pode ser convertida para a de std::functiontipo apagado, se precisar armazená-la fora do local imediatamente. chamado contexto).
Yakk - Adam Nevraumont 15/09/14
1
@tohecz Agora mencionei se ele requer C ++ 11 ou não.
precisa saber é
1
@ Yakk Oh, claro, esqueci disso! Adicionado, obrigado.
precisa saber é
1
@MooingDuck Claro que depende da implementação. Mas se bem me lembro, devido a como o apagamento de tipo funciona, há mais um indireto ocorrendo? Mas agora que eu penso sobre isso de novo, eu acho que este não é o caso, se você ponteiros de função atribuir ou lambdas para ele ... (como uma otimização típico) menos captura
leemes
1
@leemes: Certo, para ponteiros de função ou lambdas sem captura, ele deve ter a mesma sobrecarga que um c-func-ptr. O que ainda é uma paralisação do pipeline + não é trivialmente incorporado.
Mooing Duck
24

void (*callbackFunc)(int); pode ser uma função de retorno de chamada no estilo C, mas é horrivelmente inutilizável e de design ruim.

Um retorno de chamada no estilo C bem projetado parece void (*callbackFunc)(void*, int);- ele tem void*que permitir que o código que faz o retorno de chamada mantenha o estado além da função. Não fazer isso força o chamador a armazenar o estado globalmente, o que é indelicado.

std::function< int(int) >acaba sendo um pouco mais caro que a int(*)(void*, int)invocação na maioria das implementações. No entanto, é mais difícil para alguns compiladores alinharem. Existem std::functionimplementações de clones que rivalizam com a sobrecarga de invocação de ponteiros de função (consulte 'delegados mais rápidos possíveis' etc) que podem aparecer nas bibliotecas.

Agora, os clientes de um sistema de retorno de chamada geralmente precisam configurar recursos e descartá-los quando o retorno de chamada é criado e removido, e estar cientes do tempo de vida útil do retorno de chamada. void(*callback)(void*, int)não fornece isso.

Às vezes, isso está disponível através da estrutura de código (o retorno de chamada tem vida útil limitada) ou através de outros mecanismos (cancelar o registro de retornos de chamada e similares).

std::function fornece um meio para gerenciamento de vida útil limitado (a última cópia do objeto desaparece quando é esquecida).

Em geral, eu usaria um a std::functionmenos que as preocupações de desempenho se manifestassem. Se o fizessem, primeiro procuraria alterações estruturais (em vez de um retorno de chamada por pixel, que tal gerar um processador de linha de varredura baseado no lambda que você me passou? O que deve ser suficiente para reduzir a sobrecarga de chamada de função para níveis triviais. ) Então, se persistir, escreveria um delegatedelegado com base no mais rápido possível e veria se o problema de desempenho desapareceria.

Eu usaria principalmente ponteiros de função para APIs herdadas ou para criar interfaces C para comunicação entre diferentes códigos gerados por compiladores. Também os usei como detalhes de implementação interna quando estou implementando tabelas de salto, tipo apagamento, etc: quando eu estou produzindo e consumindo, e não o estou expondo externamente para nenhum código de cliente usar, e os ponteiros de função fazem tudo o que preciso .

Observe que você pode escrever wrappers que transformam um retorno de chamada std::function<int(int)>em int(void*,int)estilo, supondo que haja uma infraestrutura de gerenciamento de vida útil do retorno de chamada adequada. Portanto, como um teste de fumaça para qualquer sistema de gerenciamento de vida útil de retorno de chamada no estilo C, eu me certificaria de que o acondicionamento std::functionfuncionasse razoavelmente bem.

Yakk - Adam Nevraumont
fonte
1
De onde isso void*veio? Por que você deseja manter o estado além da função? Uma função deve conter todo o código necessário, todas as funcionalidades, basta passar os argumentos desejados e modificar e retornar algo. Se você precisar de algum estado externo, por que um functionPtr ou um retorno de chamada levará essa bagagem? Eu acho que o retorno de chamada é desnecessariamente complexo.
Nikos
@ nik-lz Não sei como ensinaria o uso e o histórico de retornos de chamada em C em um comentário. Ou a filosofia do procedimento em oposição à programação funcional. Então, você vai deixar por cumprir.
Yakk - Adam Nevraumont 26/09
Eu esqueci this. É porque é necessário considerar o caso de uma função-membro sendo chamada, então precisamos do thisponteiro para apontar para o endereço do objeto? Se eu estiver errado, você poderia me dar um link para onde posso encontrar mais informações sobre isso, porque não consigo encontrar muito sobre isso. Desde já, obrigado.
Nikos
@ As funções de membro Nik-Lz não são funções. As funções não têm estado (tempo de execução). Os retornos de chamada levam um void*para permitir a transmissão do estado de tempo de execução. Um ponteiro de função com a void*e um void*argumento pode emular uma chamada de função de membro para um objeto. Desculpe, não conheço um recurso que analise "projetar mecanismos de retorno de chamada C 101".
Yakk - Adam Nevraumont
Sim, era disso que eu estava falando. O estado de tempo de execução é basicamente o endereço do objeto que está sendo chamado (porque muda entre as execuções). Ainda é sobre this. Foi isso que eu quis dizer. OK, obrigado mesmo assim.
Nikos
17

Use std::functionpara armazenar objetos de chamada arbitrários. Ele permite que o usuário forneça o contexto necessário para o retorno de chamada; um ponteiro de função simples não.

Se você precisar usar ponteiros de função simples por algum motivo (talvez porque queira uma API compatível com C), adicione um void * user_contextargumento para que seja pelo menos possível (embora inconveniente) acessar o estado que não é passado diretamente para o função.

Mike Seymour
fonte
Qual é o tipo de p aqui? será um tipo de função std ::? void f () {}; auto p = f; p ();
sree
14

O único motivo para evitar std::functioné o suporte a compiladores legados que não têm suporte para esse modelo, que foi introduzido no C ++ 11.

Se o suporte à linguagem pré-C ++ 11 não for um requisito, o uso std::functionoferece aos seus chamadores mais opções na implementação do retorno de chamada, tornando-o uma opção melhor em comparação aos ponteiros de função "simples". Ele oferece aos usuários da sua API mais opções, enquanto abstrai as especificidades de sua implementação para o seu código que executa o retorno de chamada.

dasblinkenlight
fonte
1

std::function pode levar o VMT ao código em alguns casos, o que tem algum impacto no desempenho.

vladon
fonte
3
Você pode explicar o que é esse VMT?
Gupta