Як я можу передати функцію члена класу як зворотний дзвінок?


76

Я використовую API, який вимагає від мене передачі покажчика функції як зворотного виклику. Я намагаюся використовувати цей API зі свого класу, але отримую помилки компіляції.

Ось що я зробив від свого конструктора:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

Це не компілюється - я отримую таку помилку:

Помилка 8, помилка C3867: 'CLoggersInfra :: RedundencyManagerCallBack': у списку відсутні аргументи функції; використовувати '& CLoggersInfra :: RedundencyManagerCallBack' для створення вказівника на учасника

Я спробував пропозицію використати &CLoggersInfra::RedundencyManagerCallBack- мені не вдалося.

Будь-які пропозиції / пояснення цього ??

Я використовую VS2008.

Дякую!!

Відповіді:


55

Це не працює, оскільки покажчик функції-члена не може оброблятися як звичайний покажчик функції, оскільки він очікує аргумент об'єкта "this".

Натомість ви можете передати статичну функцію-член наступним чином, яка в цьому відношенні нагадує звичайні функції, що не є членами:

m_cRedundencyManager->Init(&CLoggersInfra::Callback, this);

Функцію можна визначити наступним чином

static void Callback(int other_arg, void * this_pointer) {
    CLoggersInfra * self = static_cast<CLoggersInfra*>(this_pointer);
    self->RedundencyManagerCallBack(other_arg);
}

4
Хоча це може бути рішенням / обхідним шляхом для ОП, я не бачу, як це відповіді на актуальне питання.
Stefan Steiger

1
@StefanSteiger відповідь (пояснення) знаходиться в останньому абзаці (по суті: "покажчик функції члена не може оброблятися як вказівник на вільну функцію"), а пропозиція, що робити ще, є в інших частинах моєї відповіді. Це правда, що це може бути більш докладно. Але це нормально, і тому моя відповідь не отримала стільки голосів, як інші. Іноді більш стислі відповіді, які по суті містять лише код, потребує допомоги краще, ніж довші, і тому мою відповідь прийняли.
Йоганнес Шауб - літб

Шауб: Так, саме так. Але я розумію - вам слід було спочатку написати останню частину, а потім сказати: замість цього ви можете зробити це + (перша частина)
Стефан Штайгер

125

Це просте запитання, але відповідь напрочуд складна. Коротка відповідь - ви можете робити те, що намагаєтесь зробити за допомогою std::bind1stабо boost::bind. Більш довга відповідь - нижче.

Компілятор правильно пропонує запропонувати вам використовувати &CLoggersInfra::RedundencyManagerCallBack. По-перше, якщо RedundencyManagerCallBackє функцією-членом, сама функція не належить до жодного конкретного екземпляра класу CLoggersInfra. Він належить самому класу. Якщо ви коли-небудь раніше викликали функцію статичного класу, можливо, ви помітили, що використовуєте той самий SomeClass::SomeMemberFunctionсинтаксис. Оскільки сама функція є "статичною" у тому сенсі, що вона належить класу, а не певному екземпляру, ви використовуєте той самий синтаксис. Значок "&" необхідний, оскільки технічно кажучи, ви не передаєте функції безпосередньо - функції не є реальними об'єктами в C ++. Натомість ви технічно передаєте адресу пам'яті для функції, тобто вказівник на те, де в пам'яті починаються інструкції функції. Наслідок той самий, хоча ви фактично "

Але це лише половина проблеми в цьому випадку. Як я вже говорив, RedundencyManagerCallBackфункція не "належить" жодному конкретному екземпляру. Але схоже, ви хочете передати його як зворотний виклик з урахуванням конкретного екземпляра. Щоб зрозуміти, як це зробити, потрібно зрозуміти, що насправді є функціями-членами: звичайними не визначеними в жодному класі функціями з додатковим прихованим параметром.

Наприклад:

class A {
public:
    A() : data(0) {}
    void foo(int addToData) { this->data += addToData; }

    int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout << an_a_object.data; // Prints 10!

Скільки параметрів A::fooпотрібно? Зазвичай ми б сказали 1. Але під капотом foo дійсно займає 2. Переглядаючи визначення A :: foo, йому потрібен конкретний екземпляр A, щоб вказівник 'this' був значущим (компілятор повинен знати, що ' це є). Як ви зазвичай вказуєте, яким ви хочете, щоб це було, відбувається через синтаксис MyObject.MyMemberFunction(). Але це лише синтаксичний цукор для передачі адреси MyObjectяк першого параметраMyMemberFunction. Подібним чином, коли ми оголошуємо функції-члени всередині визначень класів, ми не ставимо 'this' у списку параметрів, але це лише подарунок від дизайнерів мови для збереження набору тексту. Натомість вам потрібно вказати, що функція-член є статичною, щоб автоматично відмовитися від неї, отримуючи додатковий параметр 'this'. Якби компілятор С ++ переклав наведений вище приклад у код С (оригінальний компілятор С ++ насправді працював так), він, мабуть, написав би щось подібне:

struct A {
    int data;
};

void a_init(A* to_init)
{
    to_init->data = 0;
}

void a_foo(A* this, int addToData)
{ 
    this->data += addToData;
}

...

A an_a_object;
a_init(0); // Before constructor call was implicit
a_foo(&an_a_object, 5); // Used to be an_a_object.foo(5);

Повертаючись до вашого прикладу, зараз очевидна проблема. 'Init' хоче вказівник на функцію, яка приймає один параметр. Але &CLoggersInfra::RedundencyManagerCallBackце вказівник на функцію, яка приймає два параметри, це нормальний параметр і секретний параметр 'this'. Ось чому ви все ще отримуєте помилку компілятора (як додаткове зауваження: якщо ви коли-небудь використовували Python, така плутанина є тим, чому для всіх функцій-членів потрібен параметр "self").

Багатослівний спосіб вирішити це - створити спеціальний об’єкт, який утримує вказівник на екземпляр, який ви хочете, і має функцію-член, яка називається щось на зразок 'run' або 'execute' (або перевантажує оператор '()', який приймає параметри для функції-члена, а просто викликає функцію-член з цими параметрами у збереженому екземплярі. Але це зажадає, щоб ви змінили "Init", щоб взяти ваш спеціальний об'єкт, а не сирий вказівник функції, і це звучить так, як Init - чужий код. І створення спеціального класу кожного разу, коли ця проблема виникає, призведе до роздуття коду.

Отже, нарешті, хороше рішення, boost::bindа boost::functionтакож документацію до кожного з них ви можете знайти тут:

boost :: bind docs , boost :: function docs

boost::bindдозволить вам взяти функцію та параметр для цієї функції, і створити нову функцію, де цей параметр буде `` заблокований '' на місці. Отже, якщо у мене є функція, яка додає два цілих числа, я можу використати, boost::bindщоб створити нову функцію, де один із параметрів заблокований, щоб сказати 5. Ця нова функція прийме лише один цілочисельний параметр і завжди додасть 5 спеціально до нього. Використовуючи цю техніку, ви можете "зафіксувати" прихований параметр "this" як конкретний екземпляр класу та сформувати нову функцію, яка приймає лише один параметр, як ви хочете (зауважте, що прихований параметр завжди є першим параметром, і нормальні параметри впорядковуються після нього). Подивись наboost::binddocs для прикладів, вони навіть спеціально обговорюють використання його для функцій-членів. Технічно існує стандартна функція, яка називається [std::bind1st][3]і ви могли б використовувати її, але boost::bindє більш загальною.

Звичайно, є ще один улов. boost::bindзробить для вас приємну функцію boost ::, але це технічно все-таки не сирий вказівник функції, як це, мабуть, хоче Init. На щастя, boost забезпечує спосіб перетворення функції boost :: у необроблені вказівники, як описано в StackOverflow тут . Як це реалізується, виходить за рамки цієї відповіді, хоча це теж цікаво.

Не хвилюйтеся, якщо це здається смішно важким - ваше запитання перетинає декілька темних кутів С ++ і boost::bindє неймовірно корисним, коли ви його вивчите.

Оновлення C ++ 11: замість цього boost::bindви тепер можете використовувати лямбда-функцію, яка фіксує "це". Це в основному те, що компілятор генерує те саме для вас.


2
Це чудова відповідь!
aardvarkk

5
На початку відповіді як спосіб реалізації рішення пропонується std :: bind1st, але остання частина відповіді стосується виключно boost :: bind. Як можна використовувати std :: bind1st?
mabraham

1
@mabraham Ok Я додав короткий приклад, хоча він не відповідає питанню повністю (VS2008): stackoverflow.com/a/45525074/4566599
Рой Дантон,

3
Це має бути прийнятою відповіддю! Прийнята відповідь насправді не працює, якщо ви не можете змінити бібліотеку або передати необов’язкові аргументи.
Карсон МакНіл

1
@ Джозеф Гарвін: Незрозуміло, наскільки відповідь std :: bind. для цього потрібно, щоб аргумент мав тип std :: function замість звичайного вказівника на функцію C. просто тому, що ви приховуєте передачу цього, це не краще ніж прийнята відповідь. Ну добре, якщо у вас є доступ на рівні джерела до розглядуваної підписи функції, ви можете змінити foo * на std :: function <foo_signature>, і тоді вам потрібно буде лише змінити це, припускаючи, що всі компілятори оновились до C ++ 11 , але якщо у вас немає доступу до джерела, то ви F * ED, оскільки підписи несумісні. Це те саме, що лямбда-вираз на C ++.
Стефан Штайгер

21

Ця відповідь є відповіддю на коментар вище і не працює з VisualStudio 2008, але слід віддавати перевагу більш пізнім компіляторам.


У той же час ви не повинні більше використовувати порожній покажчик і там також немає необхідності для підвищення , так як std::bindі std::functionдоступні. Однією з переваг (у порівнянні з покажчиками порожнечі) є безпека типу, оскільки тип повернення та аргументи явно вказані за допомогою std::function:

// std::function<return_type(list of argument_type(s))>
void Init(std::function<void(void)> f);

Потім ви можете створити покажчик функції за допомогою std::bindі передати його Init:

auto cLoggersInfraInstance = CLoggersInfra();
auto callback = std::bind(&CLoggersInfra::RedundencyManagerCallBack, cLoggersInfraInstance);
Init(callback);

Повний приклад використання std::bindз функціями члена, статичними членами та не членами:

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

class RedundencyManager // incl. Typo ;-)
{
public:
    // std::function<return_type(list of argument_type(s))>
    std::string Init(std::function<std::string(void)> f) 
    {
        return f();
    }
};

class CLoggersInfra
{
private:
    std::string member = "Hello from non static member callback!";

public:
    static std::string RedundencyManagerCallBack()
    {
        return "Hello from static member callback!";
    }

    std::string NonStaticRedundencyManagerCallBack()
    {
        return member;
    }
};

std::string NonMemberCallBack()
{
    return "Hello from non member function!";
}

int main()
{
    auto instance = RedundencyManager();

    auto callback1 = std::bind(&NonMemberCallBack);
    std::cout << instance.Init(callback1) << "\n";

    // Similar to non member function.
    auto callback2 = std::bind(&CLoggersInfra::RedundencyManagerCallBack);
    std::cout << instance.Init(callback2) << "\n";

    // Class instance is passed to std::bind as second argument.
    // (heed that I call the constructor of CLoggersInfra)
    auto callback3 = std::bind(&CLoggersInfra::NonStaticRedundencyManagerCallBack,
                               CLoggersInfra()); 
    std::cout << instance.Init(callback3) << "\n";
}

Можливий вихід:

Hello from non member function!
Hello from static member callback!
Hello from non static member callback!

Крім того, використовуючи, std::placeholdersви можете динамічно передавати аргументи до зворотного виклику (наприклад, це дозволяє використовувати return f("MyString");in, Initякщо f має параметр рядка).


1
справжнє, справжнє велике спасибі від мене за цю відповідь! Я витрачаю вже понад дві години на пошуки та випробування різних підходів, насправді нічого не вийшло. Але цей настільки простий, що він спрацював через 1 хвилину.
BadK

3

Який аргумент Initбере? Що таке нове повідомлення про помилку?

Вказівники на методи в C ++ дещо складні у використанні. Окрім самого вказівника методу, вам також потрібно надати вказівник на екземпляр (у вашому випадку this). Може, Initочікує цього як окремого аргументу?


3

Вказівник на функцію-член класу - це не те саме, що вказівник на функцію. Член класу приймає неявний додатковий аргумент ( цей вказівник) і використовує інше узгодження виклику.

Якщо ваш API очікує функції зворотного виклику, який не є членом, ви повинні йому передати.


3

Чи m_cRedundencyManagerможна використовувати функції-члени? Більшість зворотних викликів налаштовано на використання звичайних функцій або статичних функцій-членів. Подивіться на цю сторінку в C ++ FAQ Lite для отримання додаткової інформації.

Оновлення: Оголошення функції ви надали показує , що m_cRedundencyManagerочікує функцію виду: void yourCallbackFunction(int, void *). Тому функції члена неприйнятні як зворотні дзвінки в цьому випадку. Функція статичного члена може працювати, але якщо це неприпустимо у вашому випадку, наступний код також буде працювати. Зверніть увагу, що в ньому використовується злий акторський склад void *.


// in your CLoggersInfra constructor:
m_cRedundencyManager->Init(myRedundencyManagerCallBackHandler, this);

// in your CLoggersInfra header:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr);

// in your CLoggersInfra source file:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr)
{
    ((CLoggersInfra *)CLoggersInfraPtr)->RedundencyManagerCallBack(i);
}

3

Некромантування.
Я думаю, що відповіді на сьогоднішній день трохи незрозумілі.

Давайте зробимо приклад:

Припустимо, у вас є масив пікселів (масив значень ARGB int8_t)

// A RGB image
int8_t* pixels = new int8_t[1024*768*4];

Тепер ви хочете створити PNG. Для цього ви викликаєте функцію до JPEG

bool ok = toJpeg(writeByte, pixels, width, height);

де writeByte - це функція зворотного виклику

void writeByte(unsigned char oneByte)
{
    fputc(oneByte, output);
}

Проблема тут: вихід FILE * повинен бути глобальною змінною.
Дуже погано, якщо ви перебуваєте в багатопотоковому середовищі (наприклад, на http-сервері).

Отже, вам потрібен якийсь спосіб зробити вихід не глобальною змінною, зберігаючи при цьому підпис зворотного виклику.

Безпосереднє рішення, яке приходить на розум, - це закриття, яке ми можемо наслідувати, використовуючи клас із функцією-членом.

class BadIdea {
private:
    FILE* m_stream;
public:
    BadIdea(FILE* stream)  {
        this->m_stream = stream;
    }

    void writeByte(unsigned char oneByte){
            fputc(oneByte, this->m_stream);
    }

};

А потім зробіть

FILE *fp = fopen(filename, "wb");
BadIdea* foobar = new BadIdea(fp);

bool ok = TooJpeg::writeJpeg(foobar->writeByte, image, width, height);
delete foobar;
fflush(fp);
fclose(fp);

Однак, всупереч очікуванням, це не працює.

Причина в тому, що функції-члени С ++ реалізуються на зразок функцій розширення C #.

Отже

class/struct BadIdea
{
    FILE* m_stream;
}

і

static class BadIdeaExtensions
{
    public static writeByte(this BadIdea instance, unsigned char oneByte)
    {
         fputc(oneByte, instance->m_stream);
    }

}

Отже, коли ви хочете зателефонувати writeByte, вам потрібно передати не тільки адресу writeByte, але й адресу екземпляра BadIdea.

Отже, коли у вас є typedef для процедури writeByte, і це виглядає так

typedef void (*WRITE_ONE_BYTE)(unsigned char);

І у вас є підпис writeJpeg, який виглядає так

bool writeJpeg(WRITE_ONE_BYTE output, uint8_t* pixels, uint32_t 
 width, uint32_t height))
    { ... }

принципово неможливо передати двоадресну функцію-член в одноадресний покажчик функції (без зміни writeJpeg), і немає способу обійти це.

Наступним найкращим, що ви можете зробити в C ++, є використання лямбда-функції:

FILE *fp = fopen(filename, "wb");
auto lambda = [fp](unsigned char oneByte) { fputc(oneByte, fp);  };
bool ok = TooJpeg::writeJpeg(lambda, image, width, height);

Однак, оскільки лямбда не робить нічого іншого, ніж передача екземпляра прихованому класу (наприклад, класу "BadIdea"), вам потрібно змінити підпис writeJpeg.

Перевага лямбди перед ручним класом полягає в тому, що вам просто потрібно змінити один typedef

typedef void (*WRITE_ONE_BYTE)(unsigned char);

до

using WRITE_ONE_BYTE = std::function<void(unsigned char)>; 

А потім можна залишити все інше недоторканим.

Ви також можете використовувати std :: bind

auto f = std::bind(&BadIdea::writeByte, &foobar);

Але це за кадром просто створює лямбда-функцію, яка потім також потребує змін у typedef.

Так що ні, немає можливості передати функцію-член методу, який вимагає статичного покажчика функції.

Але лямбди - це найпростіший спосіб обійтись за умови, що ви контролюєте джерело.
В іншому випадку вам не пощастило.
З C ++ ви нічого не можете зробити.

Примітка:
потрібна функція std ::#include <functional>

Однак, оскільки С ++ дозволяє використовувати і С, ви можете зробити це за допомогою libffcall у простому С, якщо ви не проти зв’язати залежність.

Завантажте libffcall з GNU (принаймні на ubuntu, не використовуйте наданий дистрибутив - він зламаний), розпакуйте.

./configure
make
make install

gcc main.c -l:libffcall.a -o ma

main.c:

#include <callback.h>

// this is the closure function to be allocated 
void function (void* data, va_alist alist)
{
     int abc = va_arg_int(alist);

     printf("data: %08p\n", data); // hex 0x14 = 20
     printf("abc: %d\n", abc);

     // va_start_type(alist[, return_type]);
     // arg = va_arg_type(alist[, arg_type]);
     // va_return_type(alist[[, return_type], return_value]);

    // va_start_int(alist);
    // int r = 666;
    // va_return_int(alist, r);
}



int main(int argc, char* argv[])
{
    int in1 = 10;

    void * data = (void*) 20;
    void(*incrementer1)(int abc) = (void(*)()) alloc_callback(&function, data);
    // void(*incrementer1)() can have unlimited arguments, e.g. incrementer1(123,456);
    // void(*incrementer1)(int abc) starts to throw errors...
    incrementer1(123);
    // free_callback(callback);
    return EXIT_SUCCESS;
}

А якщо ви використовуєте CMake, додайте бібліотеку компонувальника після add_executable

add_library(libffcall STATIC IMPORTED)
set_target_properties(libffcall PROPERTIES
        IMPORTED_LOCATION /usr/local/lib/libffcall.a)
target_link_libraries(BitmapLion libffcall)

або ви можете просто динамічно пов'язати libffcall

target_link_libraries(BitmapLion ffcall)

Примітка:
Ви можете включити заголовки libffcall та бібліотеки або створити проект cmake із вмістом libffcall.


1

Я бачу, що init має таке перевизначення:

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)

де CALLBACK_FUNC_EXє

typedef void (*CALLBACK_FUNC_EX)(int, void *);

1

Просте рішення "обхідного шляху" все-таки полягає у створенні класу віртуальних функцій "інтерфейс" та успадкуванні його в класі абонента. Потім передайте його як параметр "може бути в конструкторі" іншого класу, який ви хочете передзвонити своєму класу абонента.

ВИЗНАЧИТИ Інтерфейс:

class CallBack 
{
   virtual callMeBack () {};
};

Це клас, до якого ви хочете передзвонити:

class AnotherClass ()
{
     public void RegisterMe(CallBack *callback)
     {
         m_callback = callback;
     }

     public void DoSomething ()
     {
        // DO STUFF
        // .....
        // then call
        if (m_callback) m_callback->callMeBack();
     }
     private CallBack *m_callback = NULL;
};

І це клас, який буде передзвонено.

class Caller : public CallBack
{
    void DoSomthing ()
    {
    }

    void callMeBack()
    {
       std::cout << "I got your message" << std::endl;
    }
};

0

Це питання та відповідь із C ++ FAQ Lite охоплює ваше запитання та міркування, пов’язані з відповіддю. Короткий фрагмент з веб-сторінки, на яку я посилався:

Не робіть.

Оскільки функція-член безглузда без об’єкта, на який її можна викликати, ви не можете зробити це безпосередньо (якщо система X Window була переписана на C ++, вона, ймовірно, передавала б посилання на об’єкти навколо, а не лише вказівники на функції; природно, об’єкти буде втілювати необхідну функцію і, мабуть, набагато більше).


1
Тепер посилання є isocpp.org/wiki/faq/pointers-to-members#memfnptr-vs-fnptr ; схоже, що він зараз каже "Не треба". Ось чому відповіді лише на посилання не корисні.
lmat

2
Я змінив відповідь @LimitedAtonement. Дякуємо, що вказали на це. Ви абсолютно праві, що відповіді лише на посилання - це низькоякісні відповіді. Але ми цього не знали ще в 2008 році:
P

0

Тип вказівника на нестатичну функцію-член відрізняється від вказівника на звичайну функцію .
Тип - це void(*)(int)якщо це звичайна або статична функція-член.
Тип - це void(CLoggersInfra::*)(int)якщо це нестатична функція - член.
Отже, ви не можете передати вказівник на нестатичну функцію-член, якщо вона очікує звичайного вказівника на функцію.

Крім того, нестатична функція-член має неявний / прихований параметр для об’єкта. thisПокажчик неявно передається в якості аргументу у виклику функції - члена. Отже, функції-члени можна викликати, лише надавши об'єкт.

Якщо API Init неможливо змінити, можна використовувати функцію обгортки (звичайну функцію або функцію статичного члена класу), яка викликає член. У гіршому випадку об’єкт буде глобальним для доступу функції обгортки.

CLoggersInfra* pLoggerInfra;

RedundencyManagerCallBackWrapper(int val)
{
    pLoggerInfra->RedundencyManagerCallBack(val);
}
m_cRedundencyManager->Init(RedundencyManagerCallBackWrapper);

Якщо API Init можна змінити, існує багато альтернатив - об’єкт нестатичний покажчик функції члена, об’єкт функції,std::function або функція інтерфейсу.

Дивіться публікацію щодо зворотних викликів щодо різних варіацій робочих прикладів на C ++ .


0

Схоже, std::mem_fn(C ++ 11) робить саме те, що вам потрібно:

Шаблон функції std :: mem_fn генерує обгорткові об'єкти для вказівників на члени, які можуть зберігати, копіювати та викликати вказівник на члена. Як посилання, так і вказівники (включаючи розумні вказівники) на об'єкт можуть бути використані при виклику std :: mem_fn.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.