Как я могу передать функцию-член класса в качестве обратного вызова? - PullRequest
61 голосов
/ 30 декабря 2008

Я использую API, который требует от меня передать указатель на функцию в качестве обратного вызова. Я пытаюсь использовать этот API из своего класса, но получаю ошибки компиляции.

Вот что я сделал из моего конструктора:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

Это не компилируется - я получаю следующую ошибку:

Ошибка 8, ошибка C3867: 'CLoggersInfra :: RedundencyManagerCallBack': отсутствует список аргументов при вызове функции; используйте '& CLoggersInfra :: RedundencyManagerCallBack', чтобы создать указатель на член

Я попробовал использовать &CLoggersInfra::RedundencyManagerCallBack - у меня не получилось.

Любые предложения / объяснения этому ??

Я использую VS2008.

Спасибо !!

Ответы [ 9 ]

104 голосов
/ 31 декабря 2008

Это простой вопрос, но ответ на удивление сложный. Краткий ответ: вы можете делать то, что пытаетесь сделать с помощью 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». Если бы компилятор C ++ преобразовал приведенный выше пример в код C (оригинальный компилятор C ++ действительно работал таким образом), он, вероятно, написал бы что-то вроде этого:

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 :: bind для примеров, они даже специально обсуждают использование его для функций-членов. Технически есть стандартная функция std :: bind1st, которую вы также можете использовать, но boost :: bind является более общим.

Конечно, есть еще один улов. boost :: bind сделает для вас хорошую функцию boost ::, но технически это все же не необработанный указатель на функцию, как, вероятно, хочет Init. К счастью, boost предоставляет способ конвертации boost :: function в необработанные указатели, как описано в StackOverflow здесь . Как это реализуется, выходит за рамки этого ответа, хотя это тоже интересно.

Не беспокойтесь, если это кажется нелепо сложным - ваш вопрос пересекает несколько более темных углов C ++, и boost :: bind невероятно полезен, когда вы его изучите.

C ++ 11 update: вместо boost :: bind теперь вы можете использовать лямбда-функцию, которая захватывает 'this'. Это в основном то, что компилятор генерирует то же самое для вас.

45 голосов
/ 30 декабря 2008

Это не работает, потому что указатель на функцию-член не может быть обработан как обычный указатель на функцию, потому что он ожидает аргумент объекта 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);
}
10 голосов
/ 05 августа 2017

Этот ответ является ответом на комментарий выше и не работает с VisualStudio 2008, но его следует использовать с более поздними компиляторами.


Между тем вам больше не нужно использовать указатель пустоты, и нет необходимости в повышении, поскольку доступны std::bind и std::function. Одним преимуществом (по сравнению с указателями void) является безопасность типов, поскольку тип возвращаемого значения и аргументы явно задаются с помощью 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"); в Init, если f имеет строковый параметр).

3 голосов
/ 30 декабря 2008

Указатель на функцию-член класса не совпадает с указателем на функцию. Член класса принимает неявный дополнительный аргумент (указатель this ) и использует другое соглашение о вызовах.

Если ваш API ожидает функцию обратного вызова, не являющуюся членом, вам нужно передать ее.

3 голосов
/ 30 декабря 2008

Может ли 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 голосов
/ 30 декабря 2008

Какой аргумент принимает Init? Что такое новое сообщение об ошибке?

Указатели на методы в C ++ немного сложны в использовании. Помимо самого указателя метода, вам также необходимо предоставить указатель экземпляра (в вашем случае this). Может быть, Init ожидает это как отдельный аргумент?

1 голос
/ 30 декабря 2008

Я вижу, что init имеет следующее переопределение:

Init (CALLBACK_FUNC_EX callback_func, void * callback_parm)

где CALLBACK_FUNC_EX находится typedef void (* CALLBACK_FUNC_EX) (int, void *);

0 голосов
/ 27 марта 2019

Necromancing.
Я думаю, что ответы на сегодняшний день немного неясны.

Давайте сделаем пример:

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

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

Теперь вы хотите создать PNG. Для этого вы вызываете функцию toJpeg

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

где writeByte - функция обратного вызова

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

Проблема здесь: FILE * output должен быть глобальной переменной.
Очень плохо, если вы находитесь в многопоточной среде (например, 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 ++ реализованы подобно функциям расширения 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);

Однако, поскольку lambda не делает ничего иного, чем передача экземпляра в скрытый класс (такой как класс «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 :: function требует #include <functional>

Однако, поскольку C ++ также позволяет вам использовать C, вы можете сделать это с помощью libffcall в простом C, если вы не против связать зависимость.

Загрузите 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.

0 голосов
/ 30 декабря 2008

Этот вопрос и ответ из C ++ FAQ Lite охватывает ваш вопрос и соображения, связанные с ответом, я думаю, довольно приятно Краткий фрагмент веб-страницы, на которую я ссылался:

Не.

Потому что функция-член не имеет смысла без вызова объекта это, вы не можете сделать это напрямую (если система X Window была переписанный в C ++, он, вероятно, будет передавать ссылки на объекты вокруг, не только указатели на функции; естественно, объекты будут воплощать требуемая функция и, вероятно, намного больше).

...