Как передать функцию-член класса в качестве обратного вызова?

StackOverflow https://stackoverflow.com/questions/400257

Вопрос

Я использую 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.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top