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

102

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

以下是我从构造函数中所做的:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

这段代码编译不通过,我收到了以下错误信息:

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

我尝试了建议中使用 &CLoggersInfra::RedundencyManagerCallBack 的方法,但对我无效。

有什么建议或解释吗?

我正在使用VS2008。

谢谢!

14个回答

165
这是一个简单的问题,但答案却出乎意料地复杂。简短的答案是可以使用std::bind1stboost::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的定义,为了让'this'指针有意义(编译器需要知道'this'是什么),它需要一个特定的A实例。通常,您可以通过语法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::bindboost::function,它们各自的文档可以在此处找到:

boost::bind文档, boost::function文档

boost::bind可以让你接受一个函数和该函数的一个参数,并创建一个新函数,其中该参数被“锁定”在原位。因此,如果我有一个将两个整数相加的函数,我可以使用boost::bind来创建一个新函数,其中一个参数被锁定为5。这个新函数只需要一个整数参数,而且将始终特别添加5。使用此技术,你可以“锁定”隐藏的“this”参数以成为特定的类实例,并生成只有一个参数的新函数,就像你想要的那样(注意隐藏参数始终是第一个参数,而常规参数在其后按顺序出现)。参见boost::bind文档中的示例,它们甚至专门讨论了用于成员函数的使用方法。技术上有一个名为[std::bind1st][3]的标准函数,你也可以使用它,但boost::bind更为通用。

当然,还有一个问题。 boost::bind会为你创建一个漂亮的boost::function,但从技术上讲,这仍然不是像Init可能想要的原始函数指针。幸运的是,boost提供了一种将boost :: function转换为原始指针的方法,如StackOverflow 这里所述。它如何实现超出了本答案的范围,但也很有趣。

如果你觉得这似乎难以理解,请不必担心——你的问题涉及了C++中的几个较为阴暗的角落,并且一旦学会了就会发现boost::bind非常有用。

C++11更新:现在可以使用捕获“this”的lambda函数来替代boost::bind


2
这是一个很棒的答案! - aardvarkk
6
回答开头提到了std::bind1st作为实现解决方案的方式,但是回答的后半部分完全采用boost::bind。那么怎样使用std::bind1st呢?可以使用std::bind1st来绑定一个函数对象的第一个参数,并生成一个新的函数对象。例如,假设有一个二元函数加法器,可以将其第一个参数绑定为常数2并生成一个新的一元函数对象,代码如下:#include int add(int a, int b) { return a + b; } int main() { auto add_2 = std::bind1st(std::ptr_fun(add), 2); // 绑定add函数的第一个参数为2 int r = add_2(3); // 调用add_2相当于调用add(2, 3) return 0; }这里使用了std::ptr_fun来将函数指针转换为函数对象,从而符合std::bind1st的参数要求。因此,在本题中,如果想使用std::bind1st来实现绑定第一个参数为mstd::less<int>比较器,可以这样写:#include int main() { int m = 10; auto less_m = std::bind1st(std::less(), m); bool r = less_m(5); // 相当于调用 std::less()(m, 5) return 0; } - mabraham
3
这应该是被接受的答案!如果无法更改库或传递可选参数,则被接受的答案实际上不起作用。 - Carson McNeil
2
@Joseph Garvin:我不明白std::bind是如何解决问题的。这需要参数是std::function类型而不是普通的C函数指针。仅仅因为你隐藏了传递this,它并不比已接受的答案更好。好吧,如果你有源代码级别访问所涉及函数的签名,你可以将foo*更改为std::function<foo_signature>,然后只需要更改this,假设所有编译器都已更新到C++11,但如果你没有源代码访问权限,那么你就完蛋了,因为签名是不兼容的。这与C++ lambda表达式相同。 - Stefan Steiger
1
Clang 也在同一行给我一个错误:error: call to non-static member function without an object argument - Apollys supports Monica
显示剩余10条评论

78

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

8
虽然这可能是对原帖作者的一个解决方案或变通办法,但我无法看出这如何回答实际问题。 - Stefan Steiger
3
@StefanSteiger 的答案(解释)在最后一段中(实质上是“成员函数指针不能像自由函数指针那样处理”),而建议采取其他措施的建议则在我的回答的其他部分中。确实可以更详细地解释,但这没关系,这也是为什么我的回答没有得到像其他回答那样多的赞同票的原因。有时候,只包含所需代码的更简洁的答案比较长的答案更有帮助,这也是为什么我的回答被接受的原因。 - Johannes Schaub - litb
Schaub:是的,完全就是我的观点。但我明白了——你应该先写最后一部分,然后说:相反,你可以做这个 +(第一部分)。 - Stefan Steiger
如果你想从不同的类中回调成员函数,这种方法就行不通了(至少我找不到办法...)。 - dmedine

33

这个答案是对上面一条评论的回复,不适用于VisualStudio 2008,但应该在更近期的编译器中优先考虑。


与此同时,您不再需要使用空指针,并且也没有必要使用boost,因为std::bindstd::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,您可以动态地向回调函数传递参数(例如,这使得在Init中使用return f("MyString");如果f有一个字符串参数,则变得可行)。

1
非常感谢您的回答!我已经花费了超过两个小时搜索和尝试不同的方法,但是都没有真正起作用。但这个方法非常简单,只用了1分钟就解决了问题。 - BadK
这不需要更改 OP 没有访问权限的 RedundancyManager 代码吗? - simplename

6
仍然很难将C风格的回调函数与C ++类实例连接起来。我想重新表述一下原始问题:
  • 你正在使用的某个库需要从该库回调一个C风格的函数。改变库API是不可能的,因为它不是你的 API。

  • 你希望在自己的C++代码中处理回调,使用成员方法

由于你没有(确切地)提到要处理的回调,我将使用GLFW callbacks for key input举个例子。(顺便说一句:我知道GLFW提供了一些其他机制来附加用户数据到他们的API,但这不是这里的主题。)

我不知道任何解决此问题而不包括使用某种静态对象的解决方案。让我们看看我们的选择:

简单方法:使用C风格的全局对象

我们经常在类和实例中思考,有时忘记在C++中我们仍然可以使用整个C语言的工具。因此,有时这个非常简单的解决方案不会出现在脑海中。

假设我们有一个名为Presentation的类,应该处理键盘输入。代码可能如下所示:

struct KeyInput {
    int pressedKey;
} KeyInputGlobal;

void globalKeyHandler(GLFWwindow* window, int key, int scancode, int action, int mods) {
    KeyInputGlobal.pressedKey = key;
}

int Presentation::getCurrentKey()
{
    return KeyInputGlobal.pressedKey;
}

void Presentation::initGLFW()
{
    glfwInit();
    glfwSetKeyCallback(window, globalKeyHandler);
}

我们有一个全局对象KeyInputGlobal,应该接收按下的键。函数globalKeyHandler正好具有GLFW库调用我们代码所需的C风格API签名。它在我们的成员方法initGLFW中被激活。如果我们在代码的任何地方对当前按下的键感兴趣,我们只需调用另一个成员方法Presentation::getCurrentKey 这种方式有什么问题吗?
也许一切都很好。完全取决于你的用例。也许你只是想在应用程序代码的某个地方读取最后按下的键。你不在意错过按键事件。简单的方法就是你需要的全部。
总的来说:如果您能够在C风格代码中完全处理回调,计算出一些结果并将其存储在全局对象中以便以后从代码的其他部分读取,则确实可以使用此简单方法。优点是:它非常容易理解。缺点是?它感觉有点像作弊,因为你没有真正处理回调在你的C++代码中,你只是使用了结果。如果你认为回调是一个事件,并希望每个事件在你的成员方法中得到适当的处理,那么这种方法就不够了。

另一个简单的方法:使用C ++静态对象

我猜很多人已经这样做了。当然,我也是。想法是:等等,我们有一个C++概念的全局变量,那就是使用static。但我们可以在这里简短地讨论一下:这可能更符合C++风格,而不是使用先前示例中的C风格,但问题是相同的——我们仍然有全局变量,很难与非静态的常规成员方法结合在一起。为了完整起见,在我们的类声明中,它看起来像这样:

class Presentation
{
public:
    struct KeyInput {
        int pressedKey;
    };
    static KeyInput KeyInputGlobal;

    static void globalKeyHandler(GLFWwindow* window, int key, int scancode, int action, int mods) {
        KeyInputGlobal.pressedKey = key;
    }
    int getCurrentKey()
    {
        return KeyInputGlobal.pressedKey;
    }
...
}

激活回调的方式看起来相同,但我们还必须在实现中定义接收按键的静态结构体:
void Presentation::initGLFW()
{
    glfwInit();
    glfwSetKeyCallback(window, globalKeyHandler);
}
//static
Presentation::KeyInput Presentation::KeyInputGlobal;

您可能倾向于从回调方法globalKeyHandler中删除static关键字:编译器会立即告诉您,您不能再将其传递给glfwSetKeyCallback()。现在,如果我们只能以某种方式连接静态方法和常规方法...

C++11事件驱动方法与静态方法和lambda表达式

我找到的最佳解决方案是以下内容。它可以工作并且有点优雅,但我仍然认为它不完美。让我们看看它并讨论:

void Presentation::initGLFW()
{
    glfwInit();
    static auto callback_static = [this](
           GLFWwindow* window, int key, int scancode, int action, int mods) {
        // because we have a this pointer we are now able to call a non-static member method:
        key_callbackMember(window, key, scancode, action, mods);
    };
    glfwSetKeyCallback(window,
    [](GLFWwindow* window, int key, int scancode, int action, int mods)
      {
        // only static methods can be called here as we cannot change glfw function parameter list to include instance pointer
        callback_static(window, key, scancode, action, mods);
      }
    );
}

void Presentation::key_callbackMember(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    // we can now call anywhere in our code to process the key input:
    app->handleInput(key, action);
}
callback_static 的定义是将静态对象与实例数据连接起来,此处的 this 是我们的 Presentation 类的一个实例。你可以按照以下方式阅读该定义:如果在此定义之后任何时候调用 callback_static,则所有参数都将传递给刚使用的 Presentation 实例中调用的成员方法 key_callbackMember。这个定义与 GLFW 库没有任何关系 - 它只是下一步的准备。
现在我们使用第二个 lambda 在 glfwSetKeyCallback() 中注册回调函数。同样,如果 callback_static 没有被定义为 static,我们就不能将它传递到 GLFW 这里。
这是在所有初始化完成后,在运行时当 GLFW 调用我们的代码时会发生的事情:
  1. GLFW识别键盘事件并调用我们的静态对象callback_static
  2. callback_static可以访问Presentation类的实例,并调用其实例方法key_callbackMember
  3. 现在我们处于“对象世界”,可以在其他地方处理键盘事件。在这种情况下,我们调用某个任意对象app上的方法handleInput,该对象已在代码的其他位置设置。

优点:我们实现了所需的功能,无需在初始化方法initGLFW之外定义全局对象。不需要C风格的全局变量。

缺点:不要被一切都整齐地封装在一个方法中所迷惑。我们仍然有静态对象。以及所有全局对象所具有的问题。例如,多次调用我们的初始化方法(使用不同的Presentation实例)可能没有您预期的效果。

总结

可以将现有库的C风格回调连接到您自己代码中的类实例。您可以尝试通过在代码的成员方法中定义必要的对象来最小化 housekeeping 代码。但是,仍然需要为每个回调使用一个静态对象。如果您想将多个C++代码实例与C风格回调连接起来,请准备引入比上面示例中更复杂的静态对象管理。

希望这能帮助某些人。编码愉快。


这里有一个非常出色的贡献和全面的选项讨论。 - Weston Wedding

3
< p > Init需要传递什么参数?新错误消息是什么?< /p > < p > 在C++中,方法指针使用起来有些困难。除了方法指针本身外,您还需要提供实例指针(在您的情况下为this)。也许Init希望将其作为单独的参数传递?< /p >

3
一个指向类成员函数的指针不同于一个指向函数的指针。类成员函数会有一个隐式的额外参数(即this指针),并且使用不同的调用约定。
如果你的API需要一个非成员回调函数,那么你必须传递给它一个非成员回调函数。

3

死灵术。

我认为迄今为止的答案有些不太清晰。

让我们举个例子:

假设您有一个像素数组(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时,它看起来像这样:

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才能完成。因此,没有办法将成员函数传递给需要静态函数指针的方法。但是,如果您对源代码有控制权,则lambda是简单易行的解决方法。否则,您就没那么幸运了。在C++中你什么也做不了。
注意:std::function需要#include 但是,由于C++允许您使用C,因此,如果您不介意链接依赖项,您可以在纯C中使用libffcall(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项目。

3

m_cRedundencyManager能否使用成员函数?大多数回调函数都是设置为使用普通函数或静态成员函数。请参考C++ 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);
}

3
一个简单的解决方案“workaround”是创建一个虚函数“interface”的类,并在调用类中继承它。然后将它作为参数“可以在构造函数中”传递给想要回调你的调用者类的另一个类。
定义接口:
class CallBack 
{
   virtual callMeBack () {};
};

这是你想要回调的类:

class AnotherClass ()
{
     public void RegisterMe(CallBack *callback)
     {
         m_callback = callback;
     }

     public void DoSomething ()
     {
        // DO STUFF
        // .....
        // then call
        if (m_callback) m_callback->callMeBack();
     }
     private CallBack *m_callback = NULL;
};

这是将被回调的类。
class Caller : public CallBack
{
    void DoSomthing ()
    {
    }

    void callMeBack()
    {
       std::cout << "I got your message" << std::endl;
    }
};

2

在C++14或以上版本中,有一种令人惊讶的简单方法:

auto callback = [this](){ this->methodCB(); };
subscribeToEvent(callback);

假设subscribeToEvent获取std :: function<void()>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接