C ++의 콜백 함수


303

C ++에서 언제 어떻게 콜백 함수를 사용합니까?

편집 :
콜백 함수를 작성하는 간단한 예를보고 싶습니다.


[This] ( thispointer.com/… )는 콜백 함수에 대한 기본 사항을 잘 설명하고 개념을 이해하기 쉽습니다.
Anurag Singh

답변:


449

참고 : 대부분의 답변은 함수 포인터를 다루며 C ++에서 "콜백"논리를 달성 할 수있는 가능성은 있지만 오늘날 가장 좋은 것은 아닙니다.

콜백 (?)이란 무엇이며 왜 사용해야합니까 (!)

콜백은 클래스 또는 함수에 의해 허용되는 콜 러블 (추가 참조)이며 해당 콜백에 따라 현재 로직을 사용자 정의하는 데 사용됩니다.

콜백을 사용하는 한 가지 이유 는 호출 된 함수의 논리와 독립적이며 다른 콜백과 함께 재사용 할 수있는 일반 코드 를 작성 하는 것입니다.

표준 알고리즘 라이브러리의 많은 함수는 <algorithm>콜백을 사용합니다. 예를 들어, for_each알고리즘은 반복자 범위의 모든 항목에 단항 콜백을 적용합니다.

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

예를 들어 적절한 콜 러블을 전달하여 벡터를 먼저 증가시킨 다음 인쇄하는 데 사용할 수 있습니다.

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

인쇄

5 6.2 8 9.5 11.2

콜백의 또 다른 적용은 일정량의 정적 / 컴파일 시간 유연성을 가능하게하는 특정 이벤트를 호출자에게 알리는 것입니다.

개인적으로 두 가지 콜백을 사용하는 로컬 최적화 라이브러리를 사용합니다.

  • 함수 값과 입력 값 벡터를 기반으로하는 그래디언트가 필요한 경우 첫 번째 콜백이 호출됩니다 (논리 콜백 : 함수 값 결정 / 그라디언트 유도).
  • 두 번째 콜백은 각 알고리즘 단계마다 한 번씩 호출되며 알고리즘 수렴에 대한 특정 정보 (알림 콜백)를 수신합니다.

따라서 라이브러리 디자이너는 알림 콜백을 통해 프로그래머에게 제공되는 정보로 발생하는 정보를 결정할 책임이 없으며 로직 콜백에서 제공되므로 함수 값을 실제로 결정하는 방법에 대해 걱정할 필요가 없습니다. 이러한 것들을 올바르게 얻는 것은 라이브러리 사용자로 인한 작업이며 라이브러리를 얇고 일반적으로 유지합니다.

또한 콜백은 동적 런타임 동작을 가능하게합니다.

사용자가 키보드의 버튼과 게임 동작을 제어하는 ​​일련의 기능을 누를 때마다 실행되는 기능이있는 일종의 게임 엔진 클래스를 상상해보십시오. 콜백을 사용하면 런타임에 수행 할 작업을 (다시) 결정할 수 있습니다.

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

여기서 함수 key_pressed는 저장된 actions키를 사용하여 특정 키를 누를 때 원하는 동작을 얻습니다. 플레이어가 점프 버튼을 변경하기로 선택하면 엔진은

game_core_instance.update_keybind(newly_selected_key, &player_jump);

따라서 게임에서 다음에이 버튼을 누르면 호출 동작을 key_pressed(통화 player_jump)로 변경합니다 .

C ++ (11)에서 호출 가능한 것은 무엇입니까 ?

보다 공식적인 설명 은 C ++ 개념 : cppreference에서 호출 가능 을 참조하십시오 .

콜백 기능은 C ++ (11)에서 여러 가지 방법으로 실현 될 수 있습니다. 여러 가지 다른 것들이 호출 가능 하다는 것이 밝혀졌습니다 .

  • 함수 포인터 (멤버 ​​함수에 대한 포인터 포함)
  • std::function 사물
  • 람다 식
  • 바인드 표현식
  • 함수 객체 (오버로드 된 함수 호출 연산자가있는 클래스 operator())

* 참고 : 데이터 멤버에 대한 포인터도 호출 가능하지만 함수는 전혀 호출되지 않습니다.

콜백 을 자세하게 작성하는 몇 가지 중요한 방법

  • X.1이 포스트에서 콜백을 "쓰기"는 콜백 유형을 선언하고 이름을 지정하는 구문을 의미합니다.
  • X.2 "콜링"콜백은 해당 객체를 호출하는 구문을 나타냅니다.
  • X.3 콜백 "사용"은 콜백을 사용하여 인수를 함수에 전달할 때 구문을 의미합니다.

참고 : C ++ 17 부터 멤버 케이스에 대한 포인터를 처리하는 f(...)것과 같은 호출을 작성할 수 있습니다 std::invoke(f, ...).

1. 함수 포인터

함수 포인터는 콜백이 가질 수있는 '가장 단순하다'(일반성, 가독성은 최악)의 유형입니다.

간단한 기능을 봅시다 foo:

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

1.1 함수 포인터 / 타입 표기법 작성

함수 포인터 타입은 표기법이있다

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

어디 라는 이름의 함수 포인터 타입이 모양을

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;

using선언은 typedeffor f_int_t를 다음과 같이 작성할 수 있기 때문에 조금 더 읽기 쉬운 옵션을 제공합니다 .

using f_int_t = int(*)(int);

(적어도 나에게는) f_int_t새로운 유형 별칭이며 함수 포인터 유형의 인식이 더 쉬운 곳이 더 명확합니다.

그리고 함수 포인터 타입의 콜백을 사용한 함수 선언 은 다음과 같습니다.

// 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 콜백 통화 표기

호출 표기법은 간단한 함수 호출 구문을 따릅니다.

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 콜백 사용 표기법 및 호환 가능한 유형

함수 포인터를 사용하여 함수 포인터를 사용하는 콜백 함수를 호출 할 수 있습니다.

함수 포인터 콜백을받는 함수를 사용하는 것은 다소 간단합니다.

 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 예

콜백 작동 방식에 의존하지 않는 함수를 작성해야합니다.

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

콜백이 가능한 곳

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

처럼 사용

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. 멤버 함수의 포인터

멤버 함수에 대한 포인터 (일부 클래스의 C)는 특수 유형의 (그리고 훨씬 더 복잡한) 함수 포인터로, 유형의 객체 C가 작동해야합니다.

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

2.1 멤버 함수 / 타입 표기법에 대한 포인터 쓰기

멤버 함수 유형에 대한 포인터 일부 클래스는 T표기법이있다

// 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)

멤버 함수에 대한 명명 된 포인터 는 함수 포인터와 유사하게 다음과 같습니다.

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;

예 : 멤버 함수 콜백에 대한 포인터를 인수 중 하나로 사용 하는 함수 선언 :

// 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 콜백 통화 표기

역 참조 된 포인터에 대한 멤버 액세스 작업을 사용하여 C유형의 객체와 관련 하여 멤버에 대한 포인터 함수를 호출 할 수 있습니다 C. 참고 : 괄호가 필요합니다!

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
}

참고 :에 대한 포인터를 C사용할 수있는 경우 구문은 동일합니다 (포인터 C도 역 참조해야 함).

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 콜백 사용 표기법 및 호환 가능한 유형

클래스 T의 멤버 함수 포인터를 사용하는 콜백 함수는 class 의 멤버 함수 포인터를 사용하여 호출 할 수 있습니다 T.

멤버 함수 콜백에 대한 포인터를 취하는 함수를 사용하는 것은 함수 포인터와 유사합니다.

 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::function객체 (헤더 <functional>)

std::function클래스 저장, 복사 또는 callables 호출 다형성 함수 래퍼이다.

3.1 std::function객체 / 타입 표기법 작성

std::function콜 러블을 저장 하는 객체 의 유형은 다음과 같습니다.

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 콜백 통화 표기

클래스 std::functionoperator()대상을 호출하는 데 사용할 수있는 클래스 를 정의했습니다.

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 콜백 사용 표기법 및 호환 가능한 유형

std::function다른 유형 통과 암시 적으로 변환 할 수 있기 때문에 콜백 함수 포인터 또는 멤버 함수 포인터보다 더 일반적이기 std::function객체입니다.

3.3.1 함수 포인터와 멤버 함수에 대한 포인터

함수 포인터

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

또는 멤버 함수에 대한 포인터

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

사용할 수 있습니다.

3.3.2 람다 식

람다 식에서 명명되지 않은 클로저를 std::function객체에 저장할 수 있습니다 .

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::bind표현

std::bind식 의 결과를 전달할 수 있습니다. 예를 들어 매개 변수를 함수 포인터 호출에 바인딩하면

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 )

멤버 함수에 대한 포인터 호출을위한 객체로 객체를 바인딩 할 수도 있습니다.

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 함수 객체

operator()오버로드가 적절한 클래스 의 std::function객체도 객체 내부에 저장할 수 있습니다 .

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 예

사용할 함수 포인터 예제 변경 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]);
  }
}

(3.3 참조) 더 많은 기능을 사용할 수 있기 때문에 그 기능에 훨씬 더 많은 유용성을 제공합니다.

// 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. 템플릿 콜백 유형

템플릿을 사용하면 콜백을 호출하는 코드가 std::function객체를 사용하는 것보다 훨씬 일반적 일 수 있습니다 .

템플릿은 컴파일 타임 기능이며 컴파일 타임 다형성을위한 디자인 도구입니다. 콜백을 통해 런타임 동적 동작을 달성하려면 템플릿이 도움이되지만 런타임 동적을 유도하지는 않습니다.

4.1 쓰기 (타입 표기법) 및 템플릿 콜백 호출

std_ftransform_every_int템플릿을 사용하면 위에서부터 코드를 일반화 할 수 있습니다.

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

콜백 유형에 대한 훨씬 일반적인 (그리고 가장 쉬운) 구문을 사용하여 일반적인 템플릿 템플릿 인수로 사용하십시오.

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

참고 : 포함 된 출력은 템플릿 유형에 대해 추론 된 유형 이름을 인쇄합니다 F. 구현은 type_name이 게시물의 끝에 제공됩니다.

범위의 단항 변환에 대한 가장 일반적인 구현은 표준 라이브러리의 일부입니다. 즉 std::transform, 반복 유형과 관련하여 템플릿 화됩니다.

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 템플릿 콜백 및 호환 유형을 사용하는 예

템플릿 std::function콜백 메소드 의 호환 가능한 유형 stdf_transform_every_int_templ은 위에서 언급 한 유형과 동일합니다 (3.4 참조).

그러나 템플릿 버전을 사용하면 사용 된 콜백의 서명이 약간 변경 될 수 있습니다.

// 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);

참고 : std_ftransform_every_int(템플릿이 아닌 버전; 위 참조)은 작동 foo하지만 작동 하지 않습니다 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";
}

일반 템플릿 매개 변수는 transform_every_int_templ가능한 모든 호출 가능한 유형이 될 수 있습니다.

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

위 코드는 다음과 같이 인쇄됩니다.

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 위에서 사용 된 구현

#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 : 눈치 채지 못한 경우 : 답변에는 두 부분이 있습니다. 1. 작은 예를 들어 "콜백"에 대한 일반적인 설명. 2. 다양한 콜 러블의 전체 목록과 콜백을 사용하여 코드를 작성하는 방법. 세부 사항을 파헤 치거나 전체 답변을 읽을 수는 없지만 자세한보기를 원하지 않기 때문에 답변이 효과가 없거나 "잔인하게 복사 된"것은 아닙니다. 주제는 "c ++ 콜백"입니다. 파트 1이 OP에 적합하더라도 파트 2가 유용 할 수 있습니다. -1 대신 첫 번째 부분에 대한 정보가 부족하거나 건설적인 비판을 자유롭게 지적하십시오.
Pixelchemist

1
파트 1은 초보자에게 친숙하고 명확하지 않습니다. 나에게 무언가를 배우지 못했다고 말함으로써 더 건설적 일 수는 없다. 그리고 2 페이지는 요청되지 않았으며 페이지를 넘치게 만들었으며 의심의 여지가 없지만 그 세부 정보가 처음에 발견되는 전용 문서에서 일반적으로 발견된다는 사실에도 불구하고 유용하다고 생각합니다. 나는 확실히 downvote를 유지합니다. 단일 투표는 개인적인 의견을 나타내므로 동의하고 존중하십시오.
Bogey Jammer

24
@BogeyJammer 저는 프로그래밍에 익숙하지 않지만 "modern c ++"에 익숙하지 않습니다. 이 답변은 콜백이 수행하는 역할, 특히 C ++에 대해 추론해야 할 정확한 컨텍스트를 제공합니다. OP는 여러 가지 예를 요구하지 않았지만, 바보의 세계를 교육하려는 끊임없는 탐구에서 질문에 대한 가능한 모든 솔루션을 열거하는 것은 SO의 관습입니다. 그것이 책처럼 너무 많이 읽는다면 내가 제공 할 수있는 유일한 조언 은 몇 가지를 읽어서 조금 연습하는 입니다.
dcow

int b = foobar(a, foo); // call foobar with pointer to foo as callback, 이것은 오타 맞습니까? foo이것이 AFAIK를 작동시키기위한 포인터 여야합니다.
konoufo

[conv.func]C ++ 11 표준의 @konoufo : 는 다음 과 같이 말합니다. " 함수 유형 T의 lvalue는"포인터에서 T "유형의 prvalue로 변환 될 수 있습니다. 결과는 함수에 대한 포인터입니다. "이것은 표준 변환이며 암시 적으로 발생합니다. 물론 여기서 함수 포인터를 사용할 수 있습니다.
Pixelchemist

160

콜백을 수행하는 C 방법도 있습니다 : 함수 포인터

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

이제 클래스 메소드를 콜백으로 전달하려면 해당 함수 포인터에 대한 선언에 더 복잡한 선언이 있습니다. 예를 들면 다음과 같습니다.

//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
클래스 메소드 예제에 오류가 있습니다. 호출은 : (instance. * callback) (1.0f)
CarlJohnson

지적 해 주셔서 감사합니다. 객체를 통한 호출과 객체 포인터를 통한 호출을 설명하기 위해 둘 다 추가하겠습니다.
Ramon Zarazua B.

3
이것은 콜백이 클래스별로 입력된다는 점에서 std :: tr1 : function의 단점이 있습니다. 따라서 호출을 수행하는 객체가 호출 할 객체의 클래스를 모르는 경우 C 스타일 콜백을 사용하는 것은 실용적이지 않습니다.
bleater

typedef콜백 유형을 사용 하지 않고 어떻게 할 수 있습니까? 가능합니까?
Tomáš Zato-Reinstate Monica

1
그래 넌 할수있어. typedef더 읽기 쉬운 구문 설탕입니다. 이 없으면 typedef함수 포인터에 대한 DoWorkObject의 정의는 다음과 같습니다 void DoWorkObject(int (*callback)(float)). 회원 포인터는 다음과 같습니다.void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.

68

Scott Meyers는 좋은 예입니다.

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

나는 그 예가 전부라고 생각합니다.

std::function<> C ++ 콜백을 작성하는 "현대적인"방법입니다.


1
흥미롭게도 SM은이 책을 어떤 책으로 제공합니까? 건배 :)
sam-w

5
나는 이것이 오래되었다는 것을 알고 있지만 거의이 작업을 시작하고 설정 (mingw)에서 작동하지 않게되었습니다 .GCC 버전 <4.x를 사용하는 경우이 방법은 지원되지 않습니다. 내가 사용하는 일부 종속성은 gcc 버전> = 4.0.1에서 많은 작업없이 컴파일되지 않으므로 좋은 구식 C 스타일 콜백을 사용하는 것이 좋습니다.
OzBarry

38

콜백 함수 가 전달되는 루틴에 의해 어떤 점에서 루틴에 전달하고, 호출되는 방법이다.

이것은 재사용 가능한 소프트웨어를 만드는 데 매우 유용합니다. 예를 들어, 많은 운영 체제 API (예 : Windows API)는 콜백을 많이 사용합니다.

예를 들어, 폴더의 파일로 작업하려는 경우 자신의 루틴으로 API 함수를 호출 할 수 있으며 지정된 폴더의 파일마다 루틴이 한 번 실행됩니다. 이를 통해 API는 매우 유연합니다.


63
이 답변은 실제로 프로그래머가 모르는 것을 말하지 않습니다. 다른 많은 언어에 익숙하면서 C ++을 배우고 있습니다. 콜백은 일반적으로 관심이 없습니다.
Tomáš Zato-Reinstate Monica

17

허용되는 답변은 매우 유용하고 포괄적입니다. 그러나 OP 상태

콜백 함수를 작성하는 간단한 예 를보고 싶습니다 .

C ++ 11부터 std::function함수 포인터와 비슷한 것들이 필요하지 않습니다.

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

이 예제는 print_hashes다른 방식으로 해시 함수의 다른 구현으로 함수를 호출하려고하기 때문에 어쨌든 실제 입니다.이 목적을 위해 간단한 것을 제공했습니다. 문자열을 수신하고 int (제공된 문자열의 해시 값)를 리턴하며 구문 부분에서 기억해야 할 std::function<int (const std::string&)>것은 해당 함수를 호출하는 함수의 입력 인수로 설명합니다.


위의 모든 답변 중 콜백이 무엇인지, 어떻게 사용하는지 이해하게되었습니다. 감사.
Mehar Charan Sahai

@MeharCharanSahai 다행 이네요 :) 천만에요.
Miljen Mikic 2018 년

9

C ++에는 명시적인 콜백 함수 개념이 없습니다. 콜백 메커니즘은 종종 함수 포인터, functor 객체 또는 콜백 객체를 통해 구현됩니다. 프로그래머는 명시 적으로 콜백 기능을 설계하고 구현해야합니다.

피드백을 기반으로 편집 :

이 답변에 대한 부정적인 피드백에도 불구하고 잘못된 것은 아닙니다. 나는 어디에서 왔는지 설명하는 더 나은 일을하려고 노력할 것이다.

C 및 C ++에는 콜백 함수를 구현하는 데 필요한 모든 것이 있습니다. 콜백 함수를 구현하는 가장 일반적이고 사소한 방법은 함수 포인터를 함수 인수로 전달하는 것입니다.

그러나 콜백 함수와 함수 포인터는 동의어가 아닙니다. 함수 포인터는 언어 메커니즘이며 콜백 함수는 의미 개념입니다. 함수 포인터는 콜백 함수를 구현할 수있는 유일한 방법은 아닙니다. 또한 펑터와 정원 다양한 가상 함수를 사용할 수도 있습니다. 함수 호출을 콜백으로 만드는 것은 함수를 식별하고 호출하는 데 사용되는 메커니즘이 아니라 호출의 컨텍스트 및 의미입니다. 콜백 함수라고 말하는 것은 호출 기능과 호출되는 특정 기능 사이의 정상적인 분리, 호출자와 호출 수신자 사이의 느슨한 개념적 결합, 호출자가 호출되는 것을 명시 적으로 제어 할 수 있음을 의미합니다.

예를 들어, IFormatProvider 의 .NET 설명서에 따르면 "GetFormat은 콜백 메서드" 라고 말하지만 방금 다룬 인터페이스 방법 일뿐입니다. 나는 모든 가상 메소드 호출이 콜백 함수라고 주장하는 사람은 없다고 생각합니다. GetFormat을 콜백 메소드로 만드는 것은 그것이 전달되거나 호출되는 방식의 메커니즘이 아니라 호출자의 시맨틱이 어떤 오브젝트의 GetFormat 메소드가 호출 될지를 선택하는 것입니다.

일부 언어에는 일반적으로 이벤트 및 이벤트 처리와 관련된 명시적인 콜백 시맨틱 기능이 있습니다. 예를 들어 C #에는 콜백 개념을 중심으로 명시 적으로 설계된 구문 및 의미 체계 가있는 이벤트 유형이 있습니다. Visual Basic에는 Handles 절이 있습니다.이 절은 대리자 또는 함수 포인터의 개념을 추상화하면서 메서드를 콜백 함수로 명시 적으로 선언합니다. 이 경우 콜백의 시맨틱 개념이 언어 자체에 통합됩니다.

반면에 C와 C ++ 는 콜백 함수 의 시맨틱 개념 을 거의 명시 적으로 포함하지 않습니다 . 메커니즘이 있으며 통합 의미론은 없습니다. 콜백 함수를 잘 구현할 수는 있지만 명시적인 콜백 의미를 포함하는 더 정교한 것을 얻으려면 Qt가 Signals and Slots 와 같이 C ++에서 제공하는 것 위에 빌드해야합니다 .

간단히 말해서 C ++에는 함수 포인터를 사용하여 쉽고 간단하게 콜백을 구현하는 데 필요한 것이 있습니다. 그것이 가지고 있지 않은 것은 raise , emit , Handles , event + = 등과 같은 콜백에 고유 한 의미를 가진 키워드 및 기능입니다 . 이러한 유형의 요소가있는 언어에서 오는 경우 C ++의 기본 콜백 지원 중성 느낄 것입니다.


1
다행히도 이것은이 페이지를 방문했을 때 읽은 첫 번째 대답이 아니 었습니다. 그렇지 않으면 즉시 바운스했을 것입니다!
ubugnu

6

콜백 함수는 C 표준의 일부이므로 C ++의 일부입니다. 그러나 C ++로 작업하는 경우 옵저버 패턴을 대신 사용하는 것이 좋습니다 . http://en.wikipedia.org/wiki/Observer_pattern


1
콜백 함수는 반드시 인수로 전달 된 함수 포인터를 통해 함수를 실행하는 것과 동의어 일 필요는 없습니다. 일부 정의에서 콜백 함수라는 용어는 다른 코드에 방금 발생했거나 발생해야하는 시간을 알리는 추가 의미를 전달합니다. 이러한 관점에서 콜백 함수는 C 표준의 일부가 아니지만 표준의 일부인 함수 포인터를 사용하여 쉽게 구현할 수 있습니다.
Darryl

3
"C 표준의 일부이므로 C ++의 일부이기도합니다." 이것은 전형적인 오해이지만 그럼에도 오해 :-)
제한적 속죄

동의해야합니다. 지금 바꾸면 더 혼란스러워지기 때문에 그대로 두겠습니다. 함수 포인터 (!)가 표준의 일부라고 말하려고했습니다. 그와 다른 말을하는 것은 오해의 소지가 있습니다.
AudioDroid

콜백 함수는 어떤 방식으로 "C 표준의 일부"입니까? 함수와 함수에 대한 포인터를 지원한다는 것은 언어 개념으로 콜백을 구체적으로 정의한다는 것을 의미하지 않습니다. 또한 언급했듯이 정확하더라도 C ++과 직접 관련이 없습니다. 그리고 OP가 C ++에서 콜백을 사용할 때 "언제나 방법"을 물었을 때 (특히 절름발이의 질문이지만, 그럼에도 불구하고) 응답이 다른 경우에는 링크 전용 훈계입니다.
underscore_d

4

콜백 함수가 다른 함수로 전달되고 어떤 시점에서 호출되는 위의 정의를 참조하십시오.

C ++에서는 콜백 함수가 클래스 메소드를 호출하도록하는 것이 바람직합니다. 이렇게하면 멤버 데이터에 액세스 할 수 있습니다. 콜백을 정의하는 C 방식을 사용하는 경우 정적 멤버 함수를 가리켜 야합니다. 이것은 바람직하지 않습니다.

C ++에서 콜백을 사용하는 방법은 다음과 같습니다. 4 개의 파일을 가정하십시오. 각 클래스에 대한 .CPP / .H 파일 쌍 클래스 C1은 콜백하려는 메소드가있는 클래스입니다. C2는 C1의 메소드를 다시 호출합니다. 이 예제에서 콜백 함수는 독자를 위해 추가 한 1 개의 매개 변수를 사용합니다. 이 예제는 인스턴스화되고 사용되는 객체를 보여주지 않습니다. 이 구현의 사용 사례 중 하나는 데이터를 임시 공간으로 읽고 저장하는 클래스와 데이터를 처리하는 다른 클래스가있는 경우입니다. 콜백 함수를 사용하면 모든 데이터 행에 대해 콜백을 읽고 처리 할 수 ​​있습니다. 이 기술은 필요한 임시 공간의 오버 헤드를 줄입니다. 많은 양의 데이터를 반환 한 후 사후 처리해야하는 SQL 쿼리에 특히 유용합니다.

/////////////////////////////////////////////////////////////////////
// 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

Boost의 신호 2를 사용하면 일반적인 멤버 함수 (템플릿없이)를 스레드 안전 방식으로 구독 할 수 있습니다.

예 : 문서보기 신호를 사용하여 유연한 문서보기 아키텍처를 구현할 수 있습니다. 문서에는 각보기가 연결할 수있는 신호가 포함됩니다. 다음 Document 클래스는 여러보기를 지원하는 간단한 텍스트 문서를 정의합니다. 모든 뷰가 연결될 단일 신호를 저장합니다.

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

다음으로 뷰 정의를 시작할 수 있습니다. 다음 TextView 클래스는 문서 텍스트의 간단한보기를 제공합니다.

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

허용 된 답변은 포괄적이지만 여기서는 간단한 예를 제시하려는 질문과 관련이 있습니다. 오래 전에 작성한 코드가 있습니다. 나는 순서대로 (왼쪽 노드 다음 루트 노드 다음 오른쪽 노드) 트리를 통과하고 싶었고 한 노드에 도달 할 때마다 모든 기능을 수행 할 수 있도록 임의의 함수를 호출 할 수 있기를 원했습니다.

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
질문에 어떻게 대답합니까?
Rajan Sharma

당신은 받아 들여진 대답이 정확하고 포괄적이라는 것을 알고 있으며 일반적으로 더 이상 말할 것이 없다고 생각합니다. 하지만 콜백 함수 사용법에 대한 예를 게시했습니다.
Ehsan Ahmadi 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.