86

我正在使用一个 API,它要求我将函数指针作为回调传递。我正在尝试在我的课堂上使用这个 API,但我遇到了编译错误。

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

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

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

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

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

对此有任何建议/解释吗?

我正在使用VS2008。

谢谢!!

4

13 回答 13

143

这是一个简单的问题,但答案却出奇地复杂。std::bind1st简短的回答是你可以用or做你想做的事情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”放在参数列表中,但这只是语言设计者为节省输入而提供的礼物。相反,您必须指定成员函数是静态的,才能自动选择退出它,并自动获取额外的“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 文档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一旦您学习它就会非常有用。

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

于 2008-12-31T05:52:30.930 回答
68

这不起作用,因为成员函数指针不能像普通函数指针那样处理,因为它需要一个“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);
}
于 2008-12-30T13:55:10.410 回答
25

此答案是对上述评论的回复,不适用于 VisualStudio 2008,但应优先用于更新的编译器。


同时,您不必再使用 void 指针,也不需要 boost,因为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);

使用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");in )。Init

于 2017-08-05T18:16:05.420 回答
3

需要什么论据Init?新的错误信息是什么?

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

于 2008-12-30T13:38:17.043 回答
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);
}
于 2008-12-30T13:47:38.443 回答
3

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

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

于 2008-12-30T13:47:43.327 回答
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-instance的地址。

所以当你有一个 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 <functional>

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

从 GNU 下载 libffcall(至少在 ubuntu 上,不要使用发行版提供的包 - 它已损坏),解压缩。

./configure
make
make install

gcc main.c -l:libffcall.a -o ma

主.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 项目。

于 2019-03-27T17:49:17.727 回答
2

一个简单的“解决方法”仍然是创建一个虚函数类“接口”并在调用者类中继承它。然后将它作为参数“可能在构造函数中”传递给您想要回调调用者类的另一个类。

定义接口:

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;
    }
};
于 2019-11-03T01:08:45.470 回答
1

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

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)

CALLBACK_FUNC_EX在哪里

typedef void (*CALLBACK_FUNC_EX)(int, void *);
于 2008-12-30T13:47:03.670 回答
0

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

不。

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

于 2008-12-30T13:51:34.050 回答
0

指向非静态成员函数的指针的类型不同于指向普通函数的指针。
类型是void(*)(int)普通成员函数还是静态成员函数。
类型是void(CLoggersInfra::*)(int)如果它是一个非静态成员函数。
因此,如果需要普通函数指针,则不能将指针传递给非静态成员函数。

此外,非静态成员函数具有对象的隐式/隐藏参数。指针作为this参数隐式传递给成员函数调用。所以成员函数只能通过提供一个对象来调用。

Init 如果无法更改API ,则可以使用调用该成员的包装函数(普通函数或类静态成员函数)。在最坏的情况下,该对象将是一个供包装器函数访问的全局对象。

CLoggersInfra* pLoggerInfra;

RedundencyManagerCallBackWrapper(int val)
{
    pLoggerInfra->RedundencyManagerCallBack(val);
}
m_cRedundencyManager->Init(RedundencyManagerCallBackWrapper);

如果 APIInit 可以更改,则有许多替代方案 - Object 非静态成员函数指针、Function Objectstd::function或 Interface Function。

有关C++ 工作示例的不同变体,请参阅有关回调的帖子。

于 2020-05-08T21:16:51.333 回答
0

将 C 风格的回调函数与 C++ 类实例连接起来仍然很困难。我想改写一下原来的问题:

  • 您正在使用的某些库需要从该库回调的 C 样式函数。更改库 API 是不可能的,因为它不是您的API。

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

由于您没有(确切地)提到您要处理的回调,我将举一个使用GLFW 回调进行键输入的示例。(附带说明:我知道 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()中的 GLFW 。现在,如果我们只能以某种方式将静态方法与常规方法连接起来......

使用静态和 lambda 的 C++11 事件驱动方法

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

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的定义是我们将静态对象与实例数据连接起来,在这种情况下,是我们的Presentation类的一个实例。您可以阅读定义如下:如果在此定义之后的任何时间调用callback_static,所有参数都将传递给刚刚使用的Presentation实例调用的成员方法key_callbackMember 。这个定义与 GLFW 库没有任何关系——它只是为下一步做准备。

我们现在使用第二个 lambda 向glfwSetKeyCallback()中的库注册我们的回调。同样,如果callback_static没有被定义为静态,我们不能在这里将它传递给 GLFW。

当 GLFW 调用我们的代码时,这是在所有初始化之后在运行时发生的情况:

  1. GLFW 识别一个关键事件并调用我们的静态对象callback_static
  2. callback_static可以访问Presentation类的实例并调用它的实例方法key_callbackMember
  3. 现在我们处于“对象世界”中,我们可以在其他地方处理关键事件。在这种情况下,我们在某个任意对象app上调用方法handleInput,该对象已在我们代码的其他地方设置。

好处:我们已经实现了我们想要的,无需在初始化方法initGLFW之外定义全局对象。不需要 C 风格的全局变量。

坏处:不要仅仅因为所有东西都整齐地包装在一种方法中而被愚弄。我们仍然有静态对象。与它们一起出现的所有问题都是全局对象所具有的。例如,多次调用我们的初始化方法(使用不同的Presentation实例)可能不会达到您想要的效果。

概括

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

希望这可以帮助某人。快乐编码。

于 2022-02-20T13:05:03.663 回答
-2

看起来std::mem_fn(C++11) 完全符合您的需要:

函数模板 std::mem_fn 为指向成员的指针生成包装对象,它可以存储、复制和调用指向成员的指针。调用 std::mem_fn 时可以使用对对象的引用和指针(包括智能指针)。

于 2020-06-03T18:04:45.863 回答