Как передать функцию-член класса в качестве обратного вызова?
-
03-07-2019 - |
Вопрос
Я использую API, который требует от меня передать указатель на функцию в качестве обратного вызова.Я пытаюсь использовать этот API из своего класса, но получаю ошибки компиляции.
Вот что я сделал из своего конструктора:
m_cRedundencyManager->Init(this->RedundencyManagerCallBack);
Это не компилируется - я получаю следующую ошибку:
Ошибка 8, ошибка C3867:'CLoggersInfra::RedundencyManagerCallBack':список отсутствующих аргументов вызова функции;используйте '&CLoggersInfra::RedundencyManagerCallBack', чтобы создать указатель на член
Я попробовал предложение использовать &CLoggersInfra::RedundencyManagerCallBack
- у меня не сработало.
Любые предложения/объяснения по этому поводу??
Я использую VS2008.
Спасибо!!
Решение
Это не работает, потому что указатель на функцию-член не может обрабатываться как указатель на обычную функцию, поскольку он ожидает аргумент объекта «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);
}
Другие советы
Это простой вопрос, но ответ на удивление сложен.Короткий ответ: вы можете делать то, что пытаетесь сделать, с помощью 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 был значимым (компилятор должен знать, что такое this).Обычно вы указываете, каким должно быть «это», с помощью синтаксиса MyObject.MyMemberFunction().Но это всего лишь синтаксический сахар для передачи адреса MyObject в качестве первого параметра в MyMemberFunction.Точно так же, когда мы объявляем функции-члены внутри определений классов, мы не помещаем «это» в список параметров, но это просто подарок от разработчиков языка, позволяющий сэкономить на вводе текста.Вместо этого вам нужно указать, что функция-член является статической, чтобы автоматически отказаться от нее и получить дополнительный параметр 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).
Подробный способ справиться с этим — создать специальный объект, который содержит указатель на нужный вам экземпляр и имеет функцию-член, называемую чем-то вроде «запустить» или «выполнить» (или перегрузить оператор «()»), которая принимает параметры для функции-члена и просто вызывает функцию-член с этими параметрами в сохраненном экземпляре.Но для этого вам потребуется изменить «Init», чтобы он принимал ваш специальный объект, а не необработанный указатель на функцию, и это звучит так, будто Init — это чей-то чужой код.А создание специального класса для каждого случая возникновения этой проблемы приведет к раздуванию кода.
Итак, теперь, наконец, хорошее решение: boost::bind и boost::function, документацию для каждого из которых вы можете найти здесь:
boost::bind документы, документация boost::function
boost::bind позволит вам взять функцию и параметр этой функции и создать новую функцию, в которой этот параметр «заблокирован» на месте.Итак, если у меня есть функция, которая складывает два целых числа, я могу использовать boost::bind, чтобы создать новую функцию, в которой один из параметров заблокирован, например, 5.Эта новая функция будет принимать только один целочисленный параметр и всегда будет добавлять к нему 5.Используя этот метод, вы можете «заблокировать» скрытый параметр «this», чтобы он был конкретным экземпляром класса, и сгенерировать новую функцию, которая принимает только один параметр, как вы хотите (обратите внимание, что скрытый параметр всегда является первый параметр, а после него идут нормальные параметры).Посмотрите примеры в документации boost::bind, они даже специально обсуждают его использование для функций-членов.Технически существует стандартная функция std::bind1st, которую вы также можете использовать, но boost::bind является более общей.
Конечно, есть еще одна загвоздка.boost::bind создаст для вас хорошую функцию boost::function, но технически это все еще не простой указатель на функцию, как, вероятно, хочет Init.К счастью, boost предоставляет способ конвертировать boost::function в необработанные указатели, как описано на StackOverflow. здесь.Как это реализуется, выходит за рамки этого ответа, хотя это тоже интересно.
Не волнуйтесь, если это кажется до смешного сложным — ваш вопрос затрагивает несколько темных уголков C++, и boost::bind невероятно полезен, как только вы его изучите.
Обновление С++ 11:Вместо boost::bind теперь вы можете использовать лямбда-функцию, которая фиксирует «это».По сути, компилятор генерирует для вас то же самое.
Этот ответ является ответом на комментарий выше и не работает с VisualStudio 2008, но его следует использовать с более поздними компиляторами.
При этом вам больше не нужно использовать указатель void, и нет необходимости в повышении, поскольку 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 имеет строковый параметр).
Какой аргумент имеет Init
брать?Что такое новое сообщение об ошибке?
Указатели методов в C++ немного сложны в использовании.Помимо самого указателя метода, вам также необходимо предоставить указатель экземпляра (в вашем случае this
).Может быть Init
ожидает это как отдельный аргумент?
Указатель на функцию-член класса — это не то же самое, что указатель на функцию.Член класса принимает неявный дополнительный аргумент (теперь этот указатель) и использует другое соглашение о вызовах.
Если ваш API ожидает функцию обратного вызова, не являющуюся членом, это то, что вам нужно передать ему.
Является m_cRedundencyManager
умеете использовать функции-члены?Большинство обратных вызовов настроены на использование обычных функций или статических функций-членов.Взгляни на эта страница дополнительную информацию можно найти в FAQ по C++ 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);
}
Я вижу, что init имеет следующее переопределение:
Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)
где CALLBACK_FUNC_EX
является
typedef void (*CALLBACK_FUNC_EX)(int, void *);
Этот вопрос и ответ из Часто задаваемые вопросы по C++ Lite Я думаю, что он довольно хорошо описывает ваш вопрос и соображения, связанные с ответом.Короткий фрагмент веб-страницы, на которую я дал ссылку:
Не.
Поскольку функция -член не является бессмысленной без объекта, чтобы вызвать его, вы не можете сделать это напрямую (если система x окна была переписана в C ++, она, вероятно, будет передавать ссылки на объекты вокруг, а не только указатели на функции;Естественно, объекты будут воплощать необходимую функцию и, вероятно, намного больше).
Некромантия.
Я думаю, что ответы на сегодняшний день немного неясны.
Давайте приведем пример:
Предположим, у вас есть массив пикселей (массив значений 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++ реализованы как функции расширения 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.
Итак, когда у вас есть определение типа для процедуры 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 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
основной.с:
#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.