如何将类成员函数作为回调传递?

问题描述 投票:61回答:9

我正在使用一个API,要求我将函数指针作为回调传递。我正在尝试从我的类中使用此API,但是我遇到了编译错误。

这是我从构造函数中做的:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

这不编译 - 我收到以下错误:

错误8错误C3867:'CLoggersInfra :: RedundencyManagerCallBack':函数调用缺少参数列表;使用'&CLoggersInfra :: RedundencyManagerCallBack'创建指向成员的指针

我尝试使用&CLoggersInfra::RedundencyManagerCallBack的建议 - 对我不起作用。

对此有何建议/解释?

我正在使用VS2008。

谢谢!!

c++ callback function-pointers c++03
9个回答
46
投票

这不起作用,因为成员函数指针不能像普通函数指针那样处理,因为它需要一个“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);
}

105
投票

这是一个简单的问题,但答案非常复杂。简短的回答是你可以做你正在尝试用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()指定您想要的“this”的方式。但这只是将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 docsboost::function docs

boost :: bind将允许你获取一个函数,并为该函数提供一个参数,并在该参数被“锁定”的地方创建一个新函数。所以,如果我有一个添加两个整数的函数,我可以使用boost :: bind来创建一个新函数,其中一个参数被锁定为5。这个新函数只接受一个整数参数,并且总是会添加5个它。使用这种技术,您可以将隐藏的'this'参数“锁定”为特定的类实例,并生成一个只接受一个参数的新函数,就像您想要的那样(注意隐藏参数始终是第一个参数,并且正常参数在它之后按顺序排列。查看boost :: bind文档中的示例,他们甚至专门讨论将其用于成员函数。从技术上讲,你可以使用一个名为std :: bind1st的标准函数,但boost :: bind更通用。

当然,还有一个问题。 boost :: bind会为你做一个很好的boost :: function,但这在技术上仍然不像Init可能想要的原始函数指针。值得庆幸的是,boost提供了一种将boost :: function转换为原始指针的方法,如StackOverflow here所述。它如何实现这一点超出了这个答案的范围,尽管它也很有趣。

不要担心,如果这看起来很荒谬 - 你的问题与C ++的几个黑暗角落相交,而且一旦你学会了它,boost :: bind就非常有用了。

C ++ 11更新:您现在可以使用捕获'this'的lambda函数代替boost :: bind。这基本上让编译器为您生成相同的东西。


11
投票

这个答案是对上述评论的回复,不适用于VisualStudio 2008,但应该首选更新的编译器。


同时你不再需要使用空指针,因为std::bindstd::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);

Complete example使用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,您可以动态地将参数传递给回调(例如,如果f具有字符串参数,则可以在return f("MyString");中使用Init)。


3
投票

Init采取什么样的论据?什么是新的错误消息?

C ++中的方法指针有点难以使用。除了方法指针本身,您还需要提供一个实例指针(在您的情况下为this)。也许Init希望它作为一个单独的论点?


3
投票

指向类成员函数的指针与指向函数的指针不同。类成员采用隐式额外参数(this指针),并使用不同的调用约定。

如果您的API需要非成员回调函数,那么您必须传递给它。


3
投票

m_cRedundencyManager能够使用成员函数吗?大多数回调都设置为使用常规函数或静态成员函数。有关更多信息,请参阅C ++ FAQ Lite中的this page

更新:您提供的函数声明显示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);
}

1
投票

Necromancing。 我认为迄今为止的答案有点不清楚。

我们举一个例子:

假设你有一个像素数组(ARGB int8_t值的数组)

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

现在您要生成PNG。为此,您将该函数调用为Jpg

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时,它看起来像这样

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 ++中做的下一个最好的事情是使用lambda函数:

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的签名。

lambda优于手动类的优点是你只需要更改一个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);

但是,在幕后,这只是创建一个lambda函数,然后还需要更改typedef。

所以不,没有办法将成员函数传递给需要静态函数指针的方法。

但是,如果您可以控制源,那么lambdas就是最简单的方法。 否则,你运气不好。 你无法用C ++做任何事情。

注意: std :: function需要#include <functional>

但是,由于C ++允许您也使用C,因此如果您不介意链接依赖项,则可以使用简单C中的libffcall执行此操作。

从GNU下载libffcall(至少在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头文件和库,或者创建一个包含libffcall内容的cmake项目。


1
投票

我可以看到init具有以下覆盖:

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)

CALLBACK_FUNC_EX在哪里

typedef void (*CALLBACK_FUNC_EX)(int, void *);

0
投票

来自question and answer的这个C++ FAQ Lite涵盖了您的问题以及我认为答案中涉及的考虑因素。我链接的网页的简短片段:

别。

因为成员函数没有一个对象来调用它就没有意义,你不能直接这样做(如果X Window系统是用C ++重写的,它可能会传递对周围对象的引用,而不仅仅是指向函数的指针;自然是对象将体现所需的功能,可能还有更多功能)。

© www.soinside.com 2019 - 2024. All rights reserved.