Funções de retorno de chamada em C ++


303

No C ++, quando e como você usa uma função de retorno de chamada?

EDIT:
Gostaria de ver um exemplo simples para escrever uma função de retorno de chamada.


[Isso] ( thispointer.com/… ) explica muito bem o básico sobre as funções de retorno de chamada e é fácil entender o conceito.
Anurag Singh

Respostas:


449

Nota: A maioria das respostas abrange indicadores de função, que é uma possibilidade de obter lógica de "retorno de chamada" em C ++, mas atualmente não é a mais favorável que eu acho.

O que são retornos de chamada (?) E por que usá-los (!)

A chamada de retorno é um exigível (ver mais abaixo) aceita por uma classe ou função, usado para personalizar a lógica atual, dependendo de que callback.

Um motivo para usar retornos de chamada é escrever um código genérico independente da lógica na função chamada e pode ser reutilizado com retornos de chamada diferentes.

Muitas funções da biblioteca de algoritmos padrão <algorithm>usam retornos de chamada. Por exemplo, o for_eachalgoritmo aplica um retorno de chamada unário a cada item em um intervalo de iteradores:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
  for (; first != last; ++first) {
    f(*first);
  }
  return f;
}

que pode ser usado para incrementar primeiro e, em seguida, imprimir um vetor passando callables apropriados, por exemplo:

std::vector<double> v{ 1.0, 2.2, 4.0, 5.5, 7.2 };
double r = 4.0;
std::for_each(v.begin(), v.end(), [&](double & v) { v += r; });
std::for_each(v.begin(), v.end(), [](double v) { std::cout << v << " "; });

que imprime

5 6.2 8 9.5 11.2

Outra aplicação de retornos de chamada é a notificação de chamadores de certos eventos, o que permite uma certa flexibilidade de estática / tempo de compilação.

Pessoalmente, uso uma biblioteca de otimização local que usa dois retornos de chamada diferentes:

  • O primeiro retorno de chamada é chamado se for necessário um valor de função e o gradiente com base em um vetor de valores de entrada (retorno de chamada lógica: determinação do valor da função / derivação de gradiente).
  • O segundo retorno de chamada é chamado uma vez para cada etapa do algoritmo e recebe certas informações sobre a convergência do algoritmo (retorno de notificação).

Portanto, o designer da biblioteca não é responsável por decidir o que acontece com as informações que são fornecidas ao programador por meio do retorno de chamada de notificação e ele não precisa se preocupar em como realmente determinar os valores das funções, pois eles são fornecidos pelo retorno de chamada lógica. Consertar essas coisas é uma tarefa devido ao usuário da biblioteca e mantém a biblioteca pequena e mais genérica.

Além disso, os retornos de chamada podem ativar o comportamento dinâmico do tempo de execução.

Imagine algum tipo de classe de mecanismo de jogo que tenha uma função acionada, toda vez que os usuários pressionam um botão no teclado e um conjunto de funções que controlam o comportamento do jogo. Com retornos de chamada, você pode (re) decidir em tempo de execução qual ação será executada.

void player_jump();
void player_crouch();

class game_core
{
    std::array<void(*)(), total_num_keys> actions;
    // 
    void key_pressed(unsigned key_id)
    {
        if(actions[key_id]) actions[key_id]();
    }
    // update keybind from menu
    void update_keybind(unsigned key_id, void(*new_action)())
    {
        actions[key_id] = new_action;
    }
};

Aqui, a função key_pressedusa os retornos de chamada armazenados actionspara obter o comportamento desejado quando uma certa tecla é pressionada. Se o jogador optar por mudar o botão para pular, o mecanismo poderá chamar

game_core_instance.update_keybind(newly_selected_key, &player_jump);

e, assim, altere o comportamento de uma chamada para key_pressed( para a qual as chamadas player_jump) quando este botão for pressionado na próxima vez no jogo.

O que são callables em C ++ (11)?

Consulte Conceitos do C ++: Callable on cppreference para uma descrição mais formal.

A funcionalidade de retorno de chamada pode ser realizada de várias maneiras no C ++ (11), pois várias coisas diferentes podem ser chamadas * :

  • Ponteiros de função (incluindo ponteiros para funções-membro)
  • std::function objetos
  • Expressões lambda
  • Expressões de ligação
  • Objetos de função (classes com operador de chamada de função sobrecarregada operator())

* Nota: O ponteiro para os membros dos dados também pode ser chamado, mas nenhuma função é chamada.

Várias maneiras importantes de escrever retornos de chamada em detalhes

  • X.1 "Escrever" um retorno de chamada nesta postagem significa a sintaxe para declarar e nomear o tipo de retorno de chamada.
  • X.2 "Chamando" um retorno de chamada refere-se à sintaxe para chamar esses objetos.
  • X.3 "Usando" um retorno de chamada significa a sintaxe ao passar argumentos para uma função usando um retorno de chamada.

Nota: A partir do C ++ 17, uma chamada como f(...)pode ser escrita, pois std::invoke(f, ...)também lida com o ponteiro para o caso do membro.

1. Indicadores de função

Um ponteiro de função é o tipo mais simples (em termos de generalidade; em termos de legibilidade, sem dúvida o pior) que um retorno de chamada pode ter.

Vamos ter uma função simples foo:

int foo (int x) { return 2+x; }

1.1 Escrevendo um apontador de função / notação de tipo

Um tipo de ponteiro de função possui a notação

return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)

onde um tipo de ponteiro de função nomeado será semelhante

return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int); 

// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo; 
// can alternatively be written as 
f_int_t foo_p = &foo;

A usingdeclaração nos dá a opção de tornar as coisas um pouco mais legíveis, pois o typedeffor f_int_ttambém pode ser escrito como:

using f_int_t = int(*)(int);

Onde (pelo menos para mim) fica mais claro que f_int_té o novo alias de tipo e o reconhecimento do tipo de ponteiro de função também é mais fácil

E uma declaração de uma função usando um retorno de chamada do tipo de ponteiro de função será:

// foobar having a callback argument named moo of type 
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);

1.2 Notação de chamada de retorno

A notação de chamada segue a sintaxe da chamada de função simples:

int foobar (int x, int (*moo)(int))
{
    return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
    return x + moo(x); // function pointer moo called using argument x
}

1.3 Retorno de chamada usa notação e tipos compatíveis

Uma função de retorno de chamada usando um ponteiro de função pode ser chamada usando ponteiros de função.

O uso de uma função que recebe um retorno de chamada do ponteiro de função é bastante simples:

 int a = 5;
 int b = foobar(a, foo); // call foobar with pointer to foo as callback
 // can also be
 int b = foobar(a, &foo); // call foobar with pointer to foo as callback

1.4 Exemplo

Pode ser escrita uma função que não depende de como o retorno de chamada funciona:

void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

onde possíveis retornos de chamada possam ser

int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }

usado como

int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};

2. Ponteiro para a função membro

Um ponteiro para a função membro (de alguma classe C) é um tipo especial de (e ainda mais complexo) ponteiro de função que requer um objeto do tipo Cpara operar.

struct C
{
    int y;
    int foo(int x) const { return x+y; }
};

2.1 Escrevendo o ponteiro para a função membro / notação de tipo

Um ponteiro para o tipo de função de membro para alguma classe Tpossui a notação

// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)

onde um ponteiro nomeado para a função membro , em analogia com o ponteiro da função, fica assim:

return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x); 

// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;

Exemplo: declarando uma função usando um ponteiro para retorno de chamada da função de membro como um de seus argumentos:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2 Notação de chamada de retorno de chamada

A função ponteiro para membro de Cpode ser chamada, com relação a um objeto do tipo C, usando operações de acesso de membro no ponteiro desreferenciado. Nota: Parênteses necessários!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

Nota: Se um ponteiro para Cestiver disponível, a sintaxe é equivalente (onde o ponteiro para também Cdeve ser desreferenciado):

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3 Notação de uso de retorno de chamada e tipos compatíveis

Uma função de retorno de chamada usando um ponteiro de função de membro da classe Tpode ser chamada usando um ponteiro de função de membro da classe T.

O uso de uma função que leva um ponteiro para o retorno de chamada da função de membro também é bastante simples:

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::functionobjetos (cabeçalho <functional>)

A std::functionclasse é um invólucro de função polimórfica para armazenar, copiar ou chamar chamadas.

3.1 Escrevendo uma std::functionnotação de objeto / tipo

O tipo de um std::functionobjeto que armazena uma chamada é semelhante a:

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2 Notação de chamada de retorno

A classe std::functionfoi operator()definido, o qual pode ser utilizado para invocar o seu alvo.

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3 Retorno de chamada usa notação e tipos compatíveis

O std::functionretorno de chamada é mais genérico do que ponteiros de função ou ponteiro para função de membro, pois tipos diferentes podem ser passados ​​e implicitamente convertidos em um std::functionobjeto.

3.3.1 Ponteiros de função e ponteiros para funções-membro

Um ponteiro de função

int a = 2;
int b = stdf_foobar(a, &foo);
// b == 6 ( 2 + (2+2) )

ou um ponteiro para a função membro

int a = 2;
C my_c{7}; // aggregate initialization
int b = stdf_C_foobar(a, c, &C::foo);
// b == 11 == ( 2 + (7+2) )

pode ser usado.

3.3.2 Expressões lambda

Um fechamento sem nome de uma expressão lambda pode ser armazenado em um std::functionobjeto:

int a = 2;
int c = 3;
int b = stdf_foobar(a, [c](int x) -> int { return 7+c*x; });
// b == 15 ==  a + (7*c*a) == 2 + (7+3*2)

3.3.3 std::bindexpressões

O resultado de uma std::bindexpressão pode ser passado. Por exemplo, vinculando parâmetros a uma chamada de ponteiro de função:

int foo_2 (int x, int y) { return 9*x + y; }
using std::placeholders::_1;

int a = 2;
int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
// b == 23 == 2 + ( 9*2 + 3 )
int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
// c == 49 == 2 + ( 9*5 + 2 )

Onde também os objetos podem ser vinculados como objeto para a chamada do ponteiro para funções-membro:

int a = 2;
C const my_c{7}; // aggregate initialization
int b = stdf_foobar(a, std::bind(&C::foo, my_c, _1));
// b == 1 == 2 + ( 2 + 7 )

3.3.4 Objetos funcionais

Objetos de classes com operator()sobrecarga adequada também podem ser armazenados dentro de um std::functionobjeto.

struct Meow
{
  int y = 0;
  Meow(int y_) : y(y_) {}
  int operator()(int x) { return y * x; }
};
int a = 11;
int b = stdf_foobar(a, Meow{8});
// b == 99 == 11 + ( 8 * 11 )

3.4 Exemplo

Alterando o Exemplo do Ponteiro de Função para Usar std::function

void stdf_tranform_every_int(int * v, unsigned n, std::function<int(int)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

dá muito mais utilidade a essa função porque (ver 3.3) temos mais possibilidades de usá-la:

// using function pointer still possible
int a[5] = {1, 2, 3, 4, 5};
stdf_tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};

// use it without having to write another function by using a lambda
stdf_tranform_every_int(&a[0], 5, [](int x) -> int { return x/2; });
// now a == {1, 2, 3, 4, 5}; again

// use std::bind :
int nine_x_and_y (int x, int y) { return 9*x + y; }
using std::placeholders::_1;
// calls nine_x_and_y for every int in a with y being 4 every time
stdf_tranform_every_int(&a[0], 5, std::bind(nine_x_and_y, _1, 4));
// now a == {13, 22, 31, 40, 49};

4. Tipo de retorno de modelo

Usando modelos, o código que chama o retorno de chamada pode ser ainda mais geral do que usar std::functionobjetos.

Observe que os modelos são um recurso em tempo de compilação e uma ferramenta de design para o polimorfismo em tempo de compilação. Se o comportamento dinâmico do tempo de execução for alcançado por meio de retornos de chamada, os modelos ajudarão, mas não induzirão a dinâmica do tempo de execução.

4.1 Escrever (anotações de tipo) e chamar retornos de modelo

Generalizando, ou seja, o std_ftransform_every_intcódigo acima pode ser alcançado ainda mais usando modelos:

template<class R, class T>
void stdf_transform_every_int_templ(int * v,
  unsigned const n, std::function<R(T)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

com uma sintaxe ainda mais geral (e fácil) para um tipo de retorno de chamada, sendo um argumento de modelo simples, a ser deduzido:

template<class F>
void transform_every_int_templ(int * v, 
  unsigned const n, F f)
{
  std::cout << "transform_every_int_templ<" 
    << type_name<F>() << ">\n";
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = f(v[i]);
  }
}

Nota: A saída incluída imprime o nome do tipo deduzido para o tipo de modelo F. A implementação de type_nameé dada no final deste post.

A implementação mais geral para a transformação unária de um intervalo faz parte da biblioteca padrão, a saber std::transform, que também é modelada com relação aos tipos iterados.

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
  UnaryOperation unary_op)
{
  while (first1 != last1) {
    *d_first++ = unary_op(*first1++);
  }
  return d_first;
}

4.2 Exemplos usando retornos de chamada modelados e tipos compatíveis

Os tipos compatíveis para o std::functionmétodo de retorno de chamada modelado stdf_transform_every_int_templsão idênticos aos tipos mencionados acima (consulte 3.4).

No entanto, usando a versão do modelo, a assinatura do retorno de chamada usado pode mudar um pouco:

// Let
int foo (int x) { return 2+x; }
int muh (int const &x) { return 3+x; }
int & woof (int &x) { x *= 4; return x; }

int a[5] = {1, 2, 3, 4, 5};
stdf_transform_every_int_templ<int,int>(&a[0], 5, &foo);
// a == {3, 4, 5, 6, 7}
stdf_transform_every_int_templ<int, int const &>(&a[0], 5, &muh);
// a == {6, 7, 8, 9, 10}
stdf_transform_every_int_templ<int, int &>(&a[0], 5, &woof);

Nota: std_ftransform_every_int(versão sem modelo; veja acima) funciona com foomas não está usando muh.

// Let
void print_int(int * p, unsigned const n)
{
  bool f{ true };
  for (unsigned i = 0; i < n; ++i)
  {
    std::cout << (f ? "" : " ") << p[i]; 
    f = false;
  }
  std::cout << "\n";
}

O parâmetro de modelo simples de transform_every_int_templpode ser todo tipo de chamada possível.

int a[5] = { 1, 2, 3, 4, 5 };
print_int(a, 5);
transform_every_int_templ(&a[0], 5, foo);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, muh);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, woof);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, [](int x) -> int { return x + x + x; });
print_int(a, 5);
transform_every_int_templ(&a[0], 5, Meow{ 4 });
print_int(a, 5);
using std::placeholders::_1;
transform_every_int_templ(&a[0], 5, std::bind(foo_2, _1, 3));
print_int(a, 5);
transform_every_int_templ(&a[0], 5, std::function<int(int)>{&foo});
print_int(a, 5);

O código acima é impresso:

1 2 3 4 5
transform_every_int_templ <int(*)(int)>
3 4 5 6 7
transform_every_int_templ <int(*)(int&)>
6 8 10 12 14
transform_every_int_templ <int& (*)(int&)>
9 11 13 15 17
transform_every_int_templ <main::{lambda(int)#1} >
27 33 39 45 51
transform_every_int_templ <Meow>
108 132 156 180 204
transform_every_int_templ <std::_Bind<int(*(std::_Placeholder<1>, int))(int, int)>>
975 1191 1407 1623 1839
transform_every_int_templ <std::function<int(int)>>
977 1193 1409 1625 1841

type_name implementação usada acima

#include <type_traits>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>

template <class T>
std::string type_name()
{
  typedef typename std::remove_reference<T>::type TR;
  std::unique_ptr<char, void(*)(void*)> own
    (abi::__cxa_demangle(typeid(TR).name(), nullptr,
    nullptr, nullptr), std::free);
  std::string r = own != nullptr?own.get():typeid(TR).name();
  if (std::is_const<TR>::value)
    r += " const";
  if (std::is_volatile<TR>::value)
    r += " volatile";
  if (std::is_lvalue_reference<T>::value)
    r += " &";
  else if (std::is_rvalue_reference<T>::value)
    r += " &&";
  return r;
}

35
@ BogeyJammer: Caso você não tenha notado: A resposta tem duas partes. 1. Uma explicação geral de "retornos de chamada" com um pequeno exemplo. 2. Uma lista abrangente de chamadas diferentes e as maneiras de escrever código usando retornos de chamada. Você não pode se aprofundar nos detalhes ou ler toda a resposta, mas apenas porque não deseja uma visão detalhada, não é o caso de a resposta ser ineficaz ou "brutalmente copiada". O tópico é "retornos de chamada c ++". Mesmo se a parte 1 estiver correta para OP, outros poderão achar a parte 2 útil. Sinta-se à vontade para apontar qualquer falta de informação ou crítica construtiva para a primeira parte, em vez de -1.
precisa saber é o seguinte

1
A parte 1 não é amigável para iniciantes e suficientemente clara. Não posso ser mais construtivo dizendo que não conseguiu me aprender alguma coisa. E a parte 2 não foi solicitada, inundando a página e está fora de questão, mesmo que você pretenda que ela tenha utilidade, apesar de ser comumente encontrada em documentação dedicada, onde essas informações detalhadas são procuradas em primeiro lugar. Eu definitivamente mantenho o voto negativo. Uma única votação representa uma opinião pessoal, então aceite e respeite-a.
precisa saber é o seguinte

24
@ BogeyJammer Eu não sou novo em programação, mas sou novo em "c ++ moderno". Essa resposta me fornece o contexto exato em que preciso raciocinar sobre o papel que os retornos de chamada desempenham, especificamente em c ++. O OP pode não ter solicitado vários exemplos, mas é comum no SO, em uma busca interminável de educar um mundo de tolos, enumerar todas as soluções possíveis para uma pergunta. Se parecer muito com um livro, o único conselho que posso oferecer é praticar um pouco lendo alguns deles .
Dcow 25/02/16

int b = foobar(a, foo); // call foobar with pointer to foo as callback, este é um erro de digitação, certo? foodeve ser um ponteiro para que isso funcione no AFAIK.
Konoufo

@konoufo: [conv.func]do padrão C ++ 11 diz: " Um valor l da função tipo T pode ser convertido em um valor prvalor do tipo" ponteiro para T. " O resultado é um ponteiro para a função. "Esta é uma conversão padrão e, como tal, ocorre implicitamente. Pode-se (é claro) usar o ponteiro de função aqui.
Pixelchemist

160

Há também a maneira C de fazer retornos de chamada: ponteiros de função

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

Agora, se você deseja passar métodos de classe como retornos de chamada, as declarações para esses ponteiros de função têm declarações mais complexas, por exemplo:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}

1
Há um erro no exemplo do método de classe. A Invocação deve ser: (instância. * Retorno de chamada) (1.0f) #
244

Obrigado por apontar isso. Vou adicionar os dois para ilustrar a chamada através de um objeto e através de um ponteiro de objeto.
Ramon Zarazua B.

3
Isso tem a desvantagem de std :: tr1: function, pois o retorno de chamada é digitado por classe; isso torna impraticável usar retornos de chamada no estilo C quando o objeto que está executando a chamada não conhece a classe do objeto a ser chamado.
bleater

Como eu poderia fazer isso sem typedefo tipo de retorno de chamada? Isso é possível?
Tomáš Zato - Restabelece Monica

1
Sim você pode. typedefé apenas açúcar sintático para torná-lo mais legível. Sem typedefa definição de DoWorkObject para ponteiros de função seria: void DoWorkObject(int (*callback)(float)). Para ponteiros membros seria:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B. 16/16/15

68

Scott Meyers dá um bom exemplo:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

Eu acho que o exemplo diz tudo.

std::function<> é a maneira "moderna" de escrever retornos de chamada em C ++.


1
Fora de interesse, em qual livro a SM fornece esse exemplo? Cheers :)
sam-w

5
Eu sei que isso é antigo, mas como quase comecei a fazer isso e acabou não funcionando na minha instalação (mingw), se você estiver usando a versão do GCC <4.x, esse método não é suportado. Algumas das dependências que estou usando não serão compiladas sem muito trabalho na versão gcc> = 4.0.1, por isso estou preso ao uso de bons retornos de chamada no estilo C à moda antiga, que funcionam muito bem.
OzBarry

38

Uma função de retorno de chamada é um método que é passado para uma rotina e chamado em algum momento pela rotina para a qual é passado.

Isso é muito útil para criar software reutilizável. Por exemplo, muitas APIs do sistema operacional (como a API do Windows) usam muito os retornos de chamada.

Por exemplo, se você quiser trabalhar com arquivos em uma pasta - você pode chamar uma função API, com sua própria rotina, e sua rotina será executada uma vez por arquivo na pasta especificada. Isso permite que a API seja muito flexível.


63
Essa resposta realmente não é um programador médio que diz algo que não sabia. Estou aprendendo C ++ enquanto estou familiarizado com muitas outras línguas. O que é o retorno de chamada em geral não me diz respeito.
Tomáš Zato - Restabelece Monica

17

A resposta aceita é muito útil e bastante abrangente. No entanto, o PO afirma

Eu gostaria de ver um exemplo simples para escrever uma função de retorno de chamada.

Então, aqui está o C ++ 11, std::functionportanto, não há necessidade de ponteiros de função e coisas semelhantes:

#include <functional>
#include <string>
#include <iostream>

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

Este exemplo é, de certa forma, real, porque você deseja chamar função print_hashescom diferentes implementações de funções hash, para esse propósito forneci uma simples. Ele recebe uma string, retorna um int (um valor de hash da string fornecida) e tudo o que você precisa lembrar da parte da sintaxe é std::function<int (const std::string&)>que descreve essa função como um argumento de entrada da função que a chamará.


de todas as respostas acima, essa me fez entender o que são retornos de chamada e como usá-los. obrigado.
Mehar Charan Sahai

@MeharCharanSahai Fico feliz em ouvir isso :) De nada.
Miljen Mikic

9

Não há um conceito explícito de uma função de retorno de chamada em C ++. Os mecanismos de retorno de chamada geralmente são implementados por meio de ponteiros de função, objetos functor ou objetos de retorno de chamada. Os programadores precisam projetar e implementar explicitamente a funcionalidade de retorno de chamada.

Edite com base nos comentários:

Apesar do feedback negativo que esta resposta recebeu, não está errado. Vou tentar fazer um trabalho melhor para explicar de onde venho.

C e C ++ têm tudo o que você precisa para implementar funções de retorno de chamada. A maneira mais comum e trivial de implementar uma função de retorno de chamada é passar um ponteiro de função como argumento de função.

No entanto, funções de retorno de chamada e ponteiros de função não são sinônimos. Um ponteiro de função é um mecanismo de linguagem, enquanto uma função de retorno de chamada é um conceito semântico. Ponteiros de função não são a única maneira de implementar uma função de retorno de chamada - você também pode usar functores e até funções virtuais de variedade de jardins. O que faz uma chamada de função ser um retorno de chamada não é o mecanismo usado para identificar e chamar a função, mas o contexto e a semântica da chamada. Dizer que algo é uma função de retorno de chamada implica uma separação maior que o normal entre a função de chamada e a função específica que está sendo chamada, um acoplamento conceitual mais frouxo entre o chamador e o chamado, com o chamador tendo controle explícito sobre o que é chamado.

Por exemplo, a documentação do .NET para IFormatProvider diz que "GetFormat é um método de retorno de chamada" , mesmo que seja apenas um método de interface comum. Acho que ninguém argumentaria que todas as chamadas de método virtual são funções de retorno de chamada. O que faz de GetFormat um método de retorno de chamada não é a mecânica de como é passado ou invocado, mas a semântica do chamador que escolhe o método GetFormat de qual objeto será chamado.

Alguns idiomas incluem recursos com semântica explícita de retorno de chamada, geralmente relacionados a eventos e manipulação de eventos. Por exemplo, C # tem o tipo de evento com sintaxe e semântica explicitamente projetadas em torno do conceito de retorno de chamada. O Visual Basic possui sua cláusula Handles , que declara explicitamente um método como uma função de retorno de chamada enquanto abstrai o conceito de delegados ou ponteiros de função. Nesses casos, o conceito semântico de um retorno de chamada é integrado ao próprio idioma.

C e C ++, por outro lado, não incorporam o conceito semântico de funções de retorno de chamada quase tão explicitamente. Os mecanismos estão lá, a semântica integrada não. Você pode implementar funções de retorno de chamada muito bem, mas para obter algo mais sofisticado, que inclui a semântica explícita de retorno de chamada, você deve construí-lo sobre o que o C ++ fornece, como o que o Qt fez com seus Sinais e Slots .

Em poucas palavras, o C ++ tem o que você precisa para implementar retornos de chamada, geralmente com bastante facilidade e trivialidade usando ponteiros de função. O que ele não possui são palavras-chave e recursos cuja semântica é específica para retornos de chamada, como raise , emit , Handles , evento + = , etc. Se você vem de um idioma com esses tipos de elementos, o retorno de chamada nativo é suportado em C ++ vai se sentir castrado.


1
felizmente, essa não foi a primeira resposta que li quando visitei esta página; caso contrário, eu teria dado um salto imediato!
ubugnu

6

As funções de retorno de chamada fazem parte do padrão C e, portanto, também fazem parte do C ++. Mas se você estiver trabalhando com C ++, sugiro que você use o padrão observador : http://en.wikipedia.org/wiki/Observer_pattern


1
As funções de retorno de chamada não são necessariamente sinônimos da execução de uma função por meio de um ponteiro de função que foi passado como argumento. Por algumas definições, o termo função de retorno de chamada carrega a semântica adicional de notificar algum outro código de algo que acabou de acontecer, ou que é hora de algo acontecer. Nessa perspectiva, uma função de retorno de chamada não faz parte do padrão C, mas pode ser facilmente implementada usando ponteiros de função, que fazem parte do padrão.
Darryl

3
"parte do padrão C, portanto, também parte do C ++." Este é um mal-entendido típico, mas ainda assim é um mal-entendido :-)
Expiação limitada

Eu tenho que concordar. Vou deixar como está, pois só causará mais confusão se eu mudar agora. Eu quis dizer que o ponteiro de função (!) Faz parte do padrão. Dizer algo diferente disso - eu concordo - é enganador.
AudioDroid 26/02

De que maneira as funções de retorno de chamada "fazem parte do padrão C"? Eu não acho que o fato de ele suportar funções e ponteiros para funções significa que ele canoniza especificamente retornos de chamada como um conceito de linguagem. Além disso, como mencionado, isso não seria diretamente relevante para C ++, mesmo que fosse preciso. E isso não é especialmente relevante quando o OP perguntou "quando e como" usar retornos de chamada em C ++ (uma pergunta muito ampla e sem graça), e sua resposta é uma advertência apenas de link para fazer algo diferente.
Underscore_d

4

Veja a definição acima, onde afirma que uma função de retorno de chamada é passada para alguma outra função e, em algum momento, é chamada.

Em C ++, é desejável que funções de retorno de chamada chamem um método de classes. Ao fazer isso, você tem acesso aos dados do membro. Se você usar a maneira C de definir um retorno de chamada, precisará apontá-lo para uma função de membro estática. Isso não é muito desejável.

Aqui está como você pode usar retornos de chamada em C ++. Suponha 4 arquivos. Um par de arquivos .CPP / .H para cada classe. Classe C1 é a classe com um método que queremos chamar de retorno. C2 chama de volta ao método de C1. Neste exemplo, a função de retorno de chamada usa 1 parâmetro que eu adicionei para o bem dos leitores. O exemplo não mostra nenhum objeto sendo instanciado e usado. Um caso de uso para esta implementação é quando você tem uma classe que lê e armazena dados no espaço temporário e outra que processa os dados após a postagem. Com uma função de retorno de chamada, para cada linha de dados lida, o retorno de chamada pode processá-lo. Essa técnica elimina a sobrecarga do espaço temporário necessário. É particularmente útil para consultas SQL que retornam uma grande quantidade de dados que precisam ser pós-processados.

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}

0

Os sinais do Boost2 permitem que você assine funções-membro genéricas (sem modelos!) E de maneira segura.

Exemplo: os sinais de exibição de documento podem ser usados ​​para implementar arquiteturas flexíveis de exibição de documento. O documento conterá um sinal ao qual cada uma das visualizações pode se conectar. A seguinte classe Document define um documento de texto simples que oferece suporte a várias visualizações. Observe que ele armazena um único sinal ao qual todas as visualizações serão conectadas.

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

Em seguida, podemos começar a definir visualizações. A seguinte classe TextView fornece uma visão simples do texto do documento.

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};

0

A resposta aceita é abrangente, mas está relacionada à questão, só quero colocar um exemplo simples aqui. Eu tinha um código que escrevi há muito tempo. eu queria atravessar uma árvore de maneira ordenada (nó esquerdo, nó raiz e nó direito) e sempre que chegava a um nó, queria poder chamar uma função arbitrária para que ele pudesse fazer tudo.

void inorder_traversal(Node *p, void *out, void (*callback)(Node *in, void *out))
{
    if (p == NULL)
        return;
    inorder_traversal(p->left, out, callback);
    callback(p, out); // call callback function like this.
    inorder_traversal(p->right, out, callback);
}


// Function like bellow can be used in callback of inorder_traversal.
void foo(Node *t, void *out = NULL)
{
    // You can just leave the out variable and working with specific node of tree. like bellow.
    // cout << t->item;
    // Or
    // You can assign value to out variable like below
    // Mention that the type of out is void * so that you must firstly cast it to your proper out.
    *((int *)out) += 1;
}
// This function use inorder_travesal function to count the number of nodes existing in the tree.
void number_nodes(Node *t)
{
    int sum = 0;
    inorder_traversal(t, &sum, foo);
    cout << sum;
}

 int main()
{

    Node *root = NULL;
    // What These functions perform is inserting an integer into a Tree data-structure.
    root = insert_tree(root, 6);
    root = insert_tree(root, 3);
    root = insert_tree(root, 8);
    root = insert_tree(root, 7);
    root = insert_tree(root, 9);
    root = insert_tree(root, 10);
    number_nodes(root);
}

1
como ele responde a pergunta?
Rajan Sharma

você sabe que a resposta aceita é correta e abrangente e acho que não há mais nada a dizer em geral. mas eu posto um exemplo do meu uso de funções de retorno de chamada.
Ehsan Ahmadi
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.