Подтвердить что ты не робот

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

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

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

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

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

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

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

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

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

Спасибо!!

4b9b3361

Ответ 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.