Как передать функцию члена класса в качестве обратного вызова?
Я использую API, который требует от меня передать указатель функции как обратный вызов. Я пытаюсь использовать этот API из своего класса, но получаю ошибки компиляции.
Вот что я сделал из своего конструктора:
m_cRedundencyManager->Init(this->RedundencyManagerCallBack);
Это не компилируется - появляется следующая ошибка:
Ошибка 8 ошибки C3867: 'CLoggersInfra:: RedundencyManagerCallBack': список вызовов отсутствующих аргументов функции; используйте '& CLoggersInfra:: RedundencyManagerCallBack' для создания указателя на элемент
Я попробовал использовать &CLoggersInfra::RedundencyManagerCallBack
- не работал у меня.
Любые предложения/объяснения для этого??
Я использую VS2008.
Спасибо!!
Ответы
Ответ 1
Это не работает, потому что указатель на функцию-член не может быть обработан как обычный указатель на функцию, потому что он ожидает аргумент объекта 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);
}
Ответ 2
Это простой вопрос, но ответ на удивление сложный. Краткий ответ: вы можете делать то, что пытаетесь сделать с помощью 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 обновление: вместо boost :: bind теперь вы можете использовать лямбда-функцию, которая захватывает "this". Это в основном то, что компилятор генерирует то же самое для вас.
Ответ 3
Этот ответ является ответом на комментарий выше и не работает с VisualStudio 2008, но его следует использовать с более поздними компиляторами.
Между тем вам больше не нужно использовать указатель void, и нет необходимости в повышении, так как доступны 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");
вызову (например, это позволяет использовать return f("MyString");
в Init
если f имеет строковый параметр).
Ответ 4
Какой аргумент имеет значение Init
? Какое новое сообщение об ошибке?
Указатели методов на С++ немного сложны в использовании. Помимо самого указателя метода, вам также необходимо указать указатель на экземпляр (в вашем случае this
). Может быть, Init
ожидает его как отдельный аргумент?
Ответ 5
Может ли m_cRedundencyManager
использовать функции-члены? Большинство обратных вызовов настроены на использование регулярных функций или статических функций-членов. Посмотрите эту страницу на С++ 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);
}
Ответ 6
Указатель на функцию-член класса не совпадает с указателем на функцию. Член класса принимает неявный дополнительный аргумент (этот указатель) и использует другое соглашение о вызове.
Если ваш API ожидает функцию обратного вызова без ответа, то что вам нужно передать.
Ответ 7
Я вижу, что init имеет следующее переопределение:
Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)
где CALLBACK_FUNC_EX
typedef void (*CALLBACK_FUNC_EX)(int, void *);
Ответ 8
Этот вопрос и ответ из С++ FAQ Lite Я считаю, что ваш вопрос и соображения, связанные с ответом, довольно хорошо. Короткий фрагмент с веб-страницы, с которой я связан:
Dont.
Поскольку функция-член бессмысленна без вызова объекта он включен, вы не можете сделать это напрямую (если система X Window была переписанный на С++, он, вероятно, передаст ссылки на объекты вокруг, не только указатели на функции; естественно, объекты будут требуемая функция и, вероятно, намного больше).
Ответ 9
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 * должен быть глобальной переменной.
Очень плохо, если вы находитесь в многопоточной среде (например, 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" -class), вам необходимо изменить сигнатуру 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.