C ++ 'da geri arama işlevleri


304

C ++ 'da, ne zaman ve nasıl bir geri arama işlevi kullanıyorsunuz?

EDIT:
Geri arama işlevi yazmak için basit bir örnek görmek istiyorum.


[This] ( thispointer.com/… ) geri arama fonksiyonları hakkındaki temel kavramları çok iyi ve kolay anlaşılır biçimde açıklamaktadır.
Anurag Singh

Yanıtlar:


450

Not: Cevapların çoğu, C ++ 'da "geri arama" mantığını elde etmek için bir olasılık olan fonksiyon işaretçileri kapsar, ancak bugün itibariyle bence en uygun olanı değildir.

Geri aramalar (?) Nedir ve neden kullanılır (!)

Geri arama, o geri aramaya bağlı olarak geçerli mantığı özelleştirmek için kullanılan bir sınıf veya işlev tarafından kabul edilen çağrılabilir (aşağıya bakınız).

Geri aramaların kullanılmasının bir nedeni , çağrılan işlevdeki mantıktan bağımsız olan ve farklı geri aramalarla yeniden kullanılabilen genel kod yazmaktır .

Standart algoritmalar kitaplığının birçok işlevi <algorithm>geri çağrıları kullanır. Örneğin for_eachalgoritma, bir dizi yineleyicideki her öğeye tekli bir geri arama uygular:

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

bu, ilk önce arttırmak ve daha sonra örneğin uygun callables ileterek bir vektör yazdırmak için kullanılabilir:

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

hangi baskılar

5 6.2 8 9.5 11.2

Geri aramaların başka bir uygulaması, belirli olayların arayanlarının belirli bir miktar statik / derleme zamanı esnekliği sağlayan bildirimidir.

Şahsen, iki farklı geri arama kullanan bir yerel optimizasyon kütüphanesi kullanıyorum:

  • Bir girdi değeri vektörüne dayanan bir işlev değeri ve degrade gerekiyorsa ilk geri çağrı çağrılır (mantık geri çağırma: işlev değeri belirleme / degrade türetme).
  • İkinci geri çağrı her algoritma adımı için bir kez çağrılır ve algoritmanın yakınsaması (bildirim geri çağrısı) hakkında belirli bilgileri alır.

Bu nedenle, kütüphane tasarımcısı, bildirim geri araması yoluyla programlayıcıya verilen bilgilerle ne olacağına karar vermekle yükümlü değildir ve mantıksal geri arama tarafından sağlandıkları için işlev değerlerinin gerçekte nasıl belirleneceği konusunda endişelenmesine gerek yoktur. Bu şeyleri doğru yapmak kütüphane kullanıcısı nedeniyle bir görevdir ve kütüphaneyi ince ve daha genel tutar.

Ayrıca, geri çağrılar dinamik çalışma zamanı davranışını etkinleştirebilir.

İşe yarayan bir işlevi olan bir tür oyun motoru sınıfı düşünün, kullanıcılar klavyesinde her düğmeye bastığında ve oyun davranışınızı kontrol eden bir dizi işlev düşünün. Geri aramalarla çalışma zamanında hangi işlemin yapılacağına karar verebilirsiniz (yeniden).

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

Burada işlev key_pressed, actionsbelirli bir tuşa basıldığında istenen davranışı elde etmek için saklanan geri çağrıları kullanır . Oyuncu zıplamak için düğmeyi değiştirmeyi seçerse, motor arayabilir

game_core_instance.update_keybind(newly_selected_key, &player_jump);

ve böylece bir sonraki oyunda bu düğmeye basıldığında key_pressed(çağrının player_jump) çağrısının davranışını değiştirin .

C ++ (11) ' de callables nedir ?

Daha resmi bir açıklama için bkz. C ++ kavramları: cppreference üzerinden çağrılabilir .

Geri arama işlevselliği C ++ (11) 'te çeşitli şekillerde gerçekleştirilebilir, çünkü birkaç farklı şey çağrılabilir hale gelir * :

  • İşlev işaretçileri (üye işlevlerine işaretçiler dahil)
  • std::function nesneleri
  • Lambda ifadeleri
  • Bağlama ifadeleri
  • İşlev nesneleri (aşırı yüklenmiş işlev çağrısı işlecine sahip sınıflar operator())

* Not: Veri üyelerine işaretçi de çağrılabilir ancak hiçbir işlev çağrılmaz.

Geri çağrıları ayrıntılı olarak yazmanın birkaç önemli yolu

  • X.1 Bu gönderide "geri arama" yazmak, geri arama türünü bildirmek ve adlandırmak için sözdizimi anlamına gelir.
  • X.2 "Geri arama", bu nesneleri çağırmak için sözdizimini ifade eder.
  • X.3 Bir geri çağrıyı "kullanma", bir geri çağırma kullanarak bir işleve bağımsız değişken aktarırken sözdizimi anlamına gelir.

Not: C ++ 17'den itibaren, işaretçiyi üye vakasına da işleyen benzer bir çağrı f(...)yazılabilir std::invoke(f, ...).

1. İşlev işaretçileri

Bir işlev işaretçisi bir geri çağrının sahip olabileceği en basit (genel olarak; okunabilirlik açısından en kötü) tipidir.

Basit bir işleve sahip olalım foo:

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

1.1 Bir işlev işaretçisi / tip gösterimi yazma

Bir işlev işaretçisi türünde gösterimi vardır

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

burada adlandırılmış bir işlev işaretçisi türü

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Çünkü beyanı bize yapmak şeyler biraz daha okunabilir seçeneği sunar typedefiçin f_int_tayrıca yazılabilir:

using f_int_t = int(*)(int);

Nerede (en azından benim için) f_int_tyeni tür takma adı daha açık ve işlev işaretçisi türünün tanınması da daha kolay

Ve işlev işaretçisi türünün geri aramasını kullanan bir işlev bildirimi şöyle olacaktır:

// 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 Geri arama çağrısı notasyonu

Çağrı gösterimi, basit işlev çağrısı sözdizimini izler:

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 Geri arama kullanımı gösterimi ve uyumlu türler

İşlev işaretçisi kullanan bir geri arama işlevi, işlev işaretçileri kullanılarak çağrılabilir.

Bir işlev işaretçisi geri araması alan bir işlevi kullanmak oldukça basittir:

 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 Örnek

Geri aramanın nasıl çalıştığına bağlı olmayan bir işlev ca yazılabilir:

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

mümkünse geri aramalar olabilir

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

gibi kullanılmış

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. İşaretçi üye işlevi

İşaretçi-üye işlevi (bazı sınıflardan C), Cüzerinde çalışacak türde bir nesne gerektiren özel bir tür (ve daha karmaşık) işlev işaretleyicisidir .

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

2.1 Üye işlevine / tip göstergesine işaretçi yazma

Bazı sınıflar için bir işaretçi-üye işlev türüT gösterimi vardır

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

nerede bir üye işleve adlı işaretçi böyle fonksiyon pointer- görünüme benzetme gibi laflar edecek:

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;

Örnek: Bağımsız değişkenlerden biri olarak üye işlev geri aramasına bir işaretçi alan bir işlevi bildirme :

// 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 Geri arama çağrısı notasyonu

Öğesinin üye işlevine işaretçisi, kayıttan çıkarılmış işaretçideki üye erişim işlemleri kullanılarak Ctürdeki bir nesneye göre çağrılabilir C. Not: Parantez gereklidir!

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
}

Not: Kullanılacak bir işaretçi varsa Csözdizimi eşdeğerdir (işaretçinin Cde kaydı kaldırılmalıdır):

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 Geri arama kullanımı gösterimi ve uyumlu türler

Sınıfın üye işlev işaretçisini alan bir geri çağırma işlevi, sınıfın Tüye işlev işaretçisini kullanarak çağrılabilir T.

Üye işlev geri aramasına bir işaretçi alan bir işlev kullanmak, işlev işaretçileriyle benzerlik gösterir - oldukça basittir:

 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::functionnesneler (başlık <functional>)

std::functionSınıf deposu, kopyalama ya da callables çağırmak polimorfik bir fonksiyonu sarıcı.

3.1 Bir std::functionnesne / tip gösterimi yazma

std::functionÇağrılabilirleri saklayan bir nesnenin türü şuna benzer:

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 Geri arama çağrısı notasyonu

Sınıf std::function, operator()hedefini çağırmak için kullanılabilecek tanımlamıştır.

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 Geri arama kullanımı gösterimi ve uyumlu türler

std::functionFarklı geçirilir ve dolaylı olarak dönüştürülebilir çünkü geri arama işlevi işaretçileri veya üye işlevine işaretçi daha jenerik olduğundan, std::functionnesne.

3.3.1 İşlev işaretçileri ve üye işlevlerine işaretçiler

Bir işlev işaretçisi

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

veya üye işlevine bir işaretçi

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

kullanılabilir.

3.3.2 Lambda ifadeleri

Bir lambda ifadesinden isimsiz bir kapatma bir std::functionnesnede saklanabilir :

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İfadeler

Bir std::bindifadenin sonucu geçirilebilir. Örneğin, parametreleri bir işlev işaretçisi çağrısına bağlayarak:

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 )

Ayrıca, nesnelerin işaretçiyi üye işlevlerine çağırması için nesne olarak da bağlanabilir:

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 İşlev nesneleri

operator()Aşırı yüklenmeye sahip sınıfların nesneleri de bir std::functionnesnenin içinde saklanabilir .

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 Örnek

Kullanılacak işlev işaretçisi örneğini değiştirme 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]);
  }
}

bu işleve çok daha fazla fayda sağlar, çünkü (bkz. 3.3) onu kullanmak için daha fazla imkanımız var:

// 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. Templated geri arama türü

Şablonları kullanarak, geri aramayı çağıran kod, std::functionnesneleri kullanmaktan daha genel olabilir .

Şablonların bir derleme zamanı özelliği olduğunu ve derleme zamanı polimorfizmi için bir tasarım aracı olduğunu unutmayın. Çalışma zamanı dinamik davranışı geri çağrılar yoluyla sağlanacaksa, şablonlar yardımcı olur ancak çalışma zamanı dinamiklerini tetiklemezler.

4.1 Yazma (tip gösterimleri) ve geçici geri çağrıları arama

Genelleme yani std_ftransform_every_intyukarıdaki kod daha da ileri giderek şablonlar kullanılarak elde edilebilir:

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

geri çağrı türünün düz, çıkarılması gereken şablonlu bir argüman olması için daha genel (ve en kolay) sözdizimiyle:

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

Not: Birlikte verilen çıktı şablonlanmış tip için çıkarılan tip adını yazdırır F. Uygulaması type_namebu yazının sonunda verilmiştir.

Bir aralığın tekli dönüşümü için en genel uygulama, standart kütüphanenin bir parçasıdır, yani std::transformyinelenen türlere göre ayarlanmaktadır.

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 Geçici geri çağrıları ve uyumlu türleri kullanan örnekler

Şablonlu std::functiongeri arama yöntemi için uyumlu tipler stdf_transform_every_int_templ, yukarıda belirtilen tiplerle aynıdır (bakınız 3.4).

Bununla birlikte, şablon sürümü kullanıldığında, kullanılan geri aramanın imzası biraz değişebilir:

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

Not: std_ftransform_every_int(şablonlanmamış sürüm; yukarıya bakın) ile çalışır fooancak kullanılmaz 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";
}

Düz şablonlanmış parametre, transform_every_int_templolası her çağrılabilir tip olabilir.

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

Yukarıdaki kod yazdırılır:

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 yukarıda kullanılan uygulama

#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: Farketmediyseniz: Cevabın iki kısmı vardır. 1. Küçük bir örnekle "geri aramaların" genel açıklaması. 2. Farklı callables ve geri çağrıları kullanarak kod yazma yollarının kapsamlı bir listesi. Detaylara girmemek veya tüm cevabı okumaktan hoşlanıyorsunuz, ancak ayrıntılı bir görünüm istemediğiniz için, cevabın etkisiz veya "acımasızca kopyalandığı" durum böyle değil. Konu "c ++ geri çağrıları" dır. Bölüm 1 OP için uygun olsa bile, diğerleri bölüm 2'yi faydalı bulabilir. İlk bölüm için -1 yerine bilgi eksikliğini veya yapıcı eleştiriyi belirtmekten çekinmeyin.
Pixelchemist

1
Bölüm 1 başlangıç ​​dostu ve yeterince açık değil. Bana bir şey öğrenmeyi başaramadığını söyleyerek daha yapıcı olamam. Bölüm 2, istenmedi, sayfayı sel ve söz konusu olsa bile, söz konusu ayrıntılı bilgilerin ilk etapta arandığı özel belgelerde bulunmasına rağmen yararlıdır. Ben kesinlikle aşağı oy tutmak. Tek bir oy kişisel bir görüşü temsil eder, bu nedenle lütfen kabul edin ve saygı gösterin.
Bogey Jammer

24
@BogeyJammer Programlamaya yeni değilim ama "modern c ++" a yeniyim. Bu cevap bana geri aramaların özellikle c ++ 'da oynadığı rol hakkında aklıma gereken bağlamı veriyor. OP birden fazla örnek istememiş olabilir, ancak SO'da, bir aptallar dünyasını eğitmek, bir sorunun olası tüm çözümlerini sıralamak için hiç bitmeyen bir arayışta gelenekseldir. Bir kitap gibi çok fazla okursa, önerebileceğim tek tavsiye, birkaçını okuyarak biraz pratik yapmaktır .
dcow

int b = foobar(a, foo); // call foobar with pointer to foo as callback, bu bir yazım hatası değil mi? fooAFAIK'ın çalışması için bir işaretçi olmalıdır.
konoufo

@konoufo: [conv.func]C ++ 11 standardından şöyle diyor: " T fonksiyon tipinin bir değeri," işaretçi T "tipinde bir ön değere dönüştürülebilir. Sonuç işlevin bir göstergesidir. "Bu standart bir dönüşümdür ve dolaylı olarak gerçekleşir. Buradaki işlev işaretçisini (elbette) kullanabilirsiniz.
Pixelchemist

160

Geri arama yapmanın C yolu da vardır: işlev işaretçileri

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

Şimdi sınıf yöntemlerini geri çağrı olarak iletmek istiyorsanız, bu işlev işaretleyicilerinin bildirimlerinin daha karmaşık bildirimleri vardır, örnek:

//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
Sınıf yöntemi örneğinde bir hata var. Çağırma şöyle olmalıdır: (örnek. * Geri arama) (1.0f)
CarlJohnson

Bunu işaret ettiğiniz için teşekkür ederim. Bir nesneyi ve bir nesne işaretçisi aracılığıyla çağırmayı göstermek için her ikisini de ekleyeceğim.
Ramon Zarazua B.

3
Bunun std :: tr1: fonksiyonundan dezavantajı vardır, çünkü geri çağrı sınıf başına yazılır; bu, çağrıyı yapan nesne çağrılacak nesnenin sınıfını bilmediğinde C-tarzı geri çağrıların kullanılmasını zorlaştırır.
bleater

typedefGeri arama türünü girmeden nasıl yapabilirim ? Hatta mümkün mü?
Tomáš Zato - Monica'yı

1
Evet yapabilirsin. typedefdaha okunaklı hale getirmek için sadece sözdizimsel şekerdir. Olmadan typedef, işlev işaretçileri için DoWorkObject tanımı şöyle olacaktır: void DoWorkObject(int (*callback)(float)). Üye işaretçiler için:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.

69

Scott Meyers güzel bir örnek veriyor:

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

Bence örnek her şeyi söylüyor.

std::function<> C ++ geri aramalarını yazmanın "modern" yoludur.


1
İlgi çekici olmayan, SM bu örneği hangi kitapta veriyor? Şerefe :)
sam-w

5
Bunun eski olduğunu biliyorum, ama neredeyse bunu yapmaya başladığım ve kurulumumda (mingw) çalışmamaya başladığı için, GCC <4.x sürümünü kullanıyorsanız, bu yöntem desteklenmiyor. Kullandığım bazı bağımlılıklar, gcc sürümü> = 4.0.1'de çok fazla iş olmadan derlenmeyecek, bu yüzden iyi çalışan eski moda C tarzı geri çağrıları kullanarak sıkışıp kaldım.
OzBarry

38

Bir geri arama işlevi geçirilen edildiği rutini tarafından bir noktada bir rutine geçirildi ve adlandırılan bir yöntemdir.

Bu, yeniden kullanılabilir yazılım yapmak için çok yararlıdır. Örneğin, birçok işletim sistemi API'sı (Windows API gibi) geri aramaları yoğun olarak kullanır.

Örneğin, bir klasördeki dosyalarla çalışmak istiyorsanız - kendi rutininizle bir API işlevi çağırabilirsiniz ve rutininiz belirtilen klasördeki her dosya için bir kez çalıştırılır. Bu API'nın çok esnek olmasını sağlar.


63
Bu cevap gerçekten ortalama bir programcı bilmediği bir şey söylemiyor. Diğer birçok dile aşina olurken C ++ öğreniyorum. Geri arama genel olarak beni ilgilendirmez.
Tomáš Zato - Monica'yı

17

Kabul edilen cevap çok faydalı ve oldukça kapsamlı. Ancak, OP belirtiyor

Geri arama işlevi yazmak için basit bir örnek görmek istiyorum .

İşte burada, C ++ 11'den std::functionfonksiyon işaretçileri ve benzeri şeylere gerek yok:

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

Bu örnek bir şekilde gerçek, çünkü print_hasheshash fonksiyonlarının farklı uygulamaları ile fonksiyon çağırmak istiyorsunuz , bu amaçla basit bir tane sağladım. Bir dize alır, bir int (sağlanan dizenin karma değeri) döndürür ve sözdizimi bölümünden hatırlamanız gereken tek şey std::function<int (const std::string&)>, bu işlevi, onu çağıracak işlevin girdi argümanı olarak tanımlayan işlevdir.


Yukarıdaki yanıtların hepsinden, bu geri aramaların ne olduğunu ve nasıl kullanılacağını anlamamı sağladı. Teşekkürler.
Mehar Charan Sahai

@MeharCharanSahai Bunu duyduğuma sevindim :) Bir şey değil.
Miljen Mikic

9

C ++ 'da açık bir geri çağırma işlevi kavramı yoktur. Geri arama mekanizmaları genellikle işlev işaretçileri, işlev nesneleri veya geri arama nesneleri aracılığıyla uygulanır. Programcılar açıkça geri arama işlevselliği tasarlamalı ve uygulamalıdır.

Geri bildirime göre düzenleme:

Bu cevabın aldığı olumsuz geri bildirime rağmen yanlış değil. Nereden geldiğimi açıklamak için daha iyi bir iş yapmaya çalışacağım.

C ve C ++ geri arama işlevlerini uygulamak için ihtiyacınız olan her şeye sahiptir. Geri arama işlevi uygulamanın en yaygın ve önemsiz yolu, işlev işaretçisi işlev bağımsız değişkeni olarak iletilmesidir.

Ancak, geri arama işlevleri ve işlev işaretçileri eş anlamlı değildir. İşlev işaretçisi bir dil mekanizması iken geri arama işlevi semantik bir kavramdır. İşlev işaretçileri bir geri çağırma işlevi gerçekleştirmenin tek yolu değildir; ayrıca işlevler ve hatta bahçe çeşitliliği sanal işlevleri de kullanabilirsiniz. Bir işlevi geri arama yapan şey, işlevi tanımlamak ve çağırmak için kullanılan mekanizma değil, aramanın bağlamı ve anlamsallığıdır. Bir şey geri çağırma işlevi olduğunu söylemek, çağrı işlevi ile çağrılan özel işlev arasında normalden daha büyük bir ayırma anlamına gelir; arayan ve arayan kişi arasında daha gevşek bir kavramsal bağlantı ve arayan, çağrılan şey üzerinde açık kontrole sahiptir.

Örneğin, IFormatProvider için .NET belgeleri , yalnızca bir çalışma ortamı arabirim yöntemi olsa da, "GetFormat bir geri arama yöntemidir" diyor . Kimsenin tüm sanal yöntem çağrılarının geri çağrı işlevleri olduğunu iddia edeceğini sanmıyorum. GetFormat'ı geri arama yöntemi yapan şey, nasıl iletildiğini veya çağrıldığının mekaniği değil, arayan kişinin hangi nesnenin GetFormat yönteminin çağrıldığını seçmesinin anlambilimidir.

Bazı diller, genellikle olaylar ve olay işleme ile ilişkili açık geri arama semantiği olan özellikler içerir. Örneğin, C # sözdizimi ve semantik ile geri çağrı kavramı etrafında açıkça tasarlanmış olay türüne sahiptir. Visual Basic, delege veya işlev işaretçisi kavramını soyutlarken bir geri çağırma işlevi olarak açıkça bir yöntem bildiren kendi Handles yan tümcesi vardır . Bu durumlarda, semantik geri arama kavramı dilin kendisine entegre edilir.

C ve C ++ ise, geri arama işlevlerinin semantik kavramını neredeyse açık bir şekilde gömmez . Mekanizmalar orada, entegre anlambilim de yok. Geri arama işlevlerini gayet iyi uygulayabilirsiniz, ancak açık geri çağrı semantiği içeren daha karmaşık bir şey elde etmek için, C ++'ın Sinyalleri ve Yuvaları ile ne yaptığı gibi C ++'ın sağladığı şeylerin üzerine inşa etmeniz gerekir .

Özetle, C ++, genellikle kolayca ve önemsiz bir şekilde işlev işaretçileri kullanarak geri çağrıları uygulamak için ihtiyacınız olan her şeye sahiptir. Semantikleri yükselt , yay , Kollar , olay + = , vb. Gibi geri çağrılara özgü anahtar kelimeler ve özelliklerdir . Bu tür öğelere sahip bir dilden geliyorsanız, C ++ 'da yerel geri arama desteği kısır hissedecek.


1
Neyse ki bu sayfayı ziyaret ettiğimde okuduğum ilk cevap değildi, aksi halde hemen geri dönerdim!
ubugnu

6

Geri arama işlevleri C standardının bir parçasıdır, bu nedenle C ++ 'ın bir parçasıdır. Ancak C ++ ile çalışıyorsanız, bunun yerine gözlemci desenini kullanmanızı öneririm : http://en.wikipedia.org/wiki/Observer_pattern


1
Geri çağrı işlevleri, bağımsız değişken olarak iletilen bir işlev işaretçisi aracılığıyla bir işlevin yürütülmesi ile eş anlamlı değildir. Bazı tanımlarla, geri çağırma işlevi terimi, yeni gerçekleşen bir şeyin başka bir kodunu bildirmenin ek anlambilimini veya bir şeyin olması gereken zamandır. Bu açıdan bakıldığında, bir geri çağırma işlevi C standardının bir parçası değildir, ancak standardın bir parçası olan işlev işaretçileri kullanılarak kolayca uygulanabilir.
Darryl

3
"C standardının bir parçası, bu nedenle C ++ 'ın bir parçası." Bu tipik bir yanlış anlama, ama yine de bir yanlış anlama :-)
Sınırlı Kefaret

Katılıyorum. Olduğu gibi bırakacağım, çünkü sadece şimdi değiştirirsem daha fazla karışıklığa neden olacak. İşlev işaretçisinin (!) Standardın bir parçası olduğunu söylemek istedim. Bundan farklı bir şey söylemek - katılıyorum - yanıltıcıdır.
AudioDroid

Geri arama işlevleri "C standardının bir parçası" ne şekilde? İşlevleri ve işlevlere işaret etmeyi desteklediği gerçeğinin, bir dil kavramı olarak geri çağrıları özellikle canonlaştırdığı anlamına gelmez. Ayrıca, belirtildiği gibi, doğru olsa bile, bu doğrudan C ++ ile ilgili olmayacaktır. Ve özellikle OP, C ++ 'da (geri dönüşümlü, çok geniş bir soru, ancak yine de) geri çağrıları "ne zaman ve nasıl" istediğini sorduğunda önemli değildir ve cevabınız bunun yerine farklı bir şey yapmak için sadece bağlantı içeren bir uyarıcıdır.
underscore_d

4

Geri arama işlevinin başka bir işleve iletildiğini ve bir noktada çağrıldığını belirten yukarıdaki tanıma bakın.

C ++ 'da geri çağırma işlevlerinin bir sınıflar yöntemi çağırması istenir. Bunu yaptığınızda üye verilerine erişebilirsiniz. Bir geri çağırma tanımlamak için C yöntemini kullanırsanız, bunu statik bir üye işlevine yönlendirmeniz gerekir. Bu çok arzu edilen bir durum değil.

C ++ 'da geri çağrıları nasıl kullanabileceğiniz aşağıda açıklanmıştır. 4 dosya varsayın. Her sınıf için bir çift .CPP / .H dosyası. C1 Sınıfı, geri çağırmak istediğimiz yöntemi içeren sınıftır. C2, C1'in yöntemini geri çağırır. Bu örnekte, geri arama işlevi, okuyucu uğruna eklediğim 1 parametreyi alır. Örnek, somutlaştırılan ve kullanılan hiçbir nesneyi göstermez. Bu uygulama için bir kullanım örneği, verileri okuyan ve geçici alana depolayan bir sınıfınız ve verileri işleyen bir sınıfınız olduğundadır. Bir geri arama fonksiyonu ile, her veri satırı için geri aramayı okuyabilir. Bu teknik, gerekli geçici alanın ek yükünü keser. Özellikle sonradan işlenmesi gereken büyük miktarda veri döndüren SQL sorguları için kullanışlıdır.

/////////////////////////////////////////////////////////////////////
// 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 signals2 sen (! Şablonlar olmadan) ve evre şekilde jenerik üye işlevlerini abone olmalarını sağlar.

Örnek: Belge Görünümü Sinyalleri, esnek Belge Görünümü mimarilerini uygulamak için kullanılabilir. Belge, görünümlerin her birinin bağlanabileceği bir sinyal içerecektir. Aşağıdaki Document sınıfı, çoklu görünümleri destekleyen basit bir metin belgesi tanımlar. Tüm görünümlerin bağlanacağı tek bir sinyal sakladığını unutmayın.

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

Ardından, görünümleri tanımlamaya başlayabiliriz. Aşağıdaki TextView sınıfı, belge metninin basit bir görünümünü sağlar.

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

Kabul edilen cevap kapsamlı ama soru ile ilgili sadece basit bir örnek koymak istiyorum. Uzun zaman önce yazdığım bir kod vardı. sırayla (sol-düğüm sonra kök-düğüm sonra sağ-düğüm) bir ağaç üzerinden geçmek istedim ve ne zaman bir düğüme ulaşmak her şeyi yapabilmek için rasgele bir işlev çağırmak istedim.

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
soruyu nasıl cevaplıyor?
Rajan Sharma

Kabul edilen cevabın doğru ve kapsamlı olduğunu biliyorsunuz ve genel olarak söyleyecek başka bir şey olmadığını düşünüyorum. ancak geri arama işlevlerini kullanmamın bir örneğini gönderiyorum.
Ehsan Ahmadi
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.