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


76

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

这是我从构造函数执行的操作:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

无法编译-我收到以下错误:

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

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

任何建议/解释吗?

我正在使用VS2008。

谢谢!!

Answers:


54

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

4
尽管这可能是OP的解决方案/解决方法,但我看不出这是如何解决实际问题的。
Stefan Steiger,

1
@StefanSteiger答案(解释)在最后一段中(本质上是:“成员函数指针不能像指向自由函数的指针一样进行处理”),其他建议则在我的答案的其他部分。的确,它可能更详尽。但这没关系,这就是为什么我的答案没有其他答案那么多的原因。有时,更简洁的答案实际上只包含所需的代码,而比更长的答案更好,这就是为什么我的答案被接受的原因。
Johannes Schaub-litb

绍布:是的,正是我的意思。但我知道-您应该先写最后一部分,然后说:相反,您可以这样做+(第一部分)
Stefan Steiger

123

这是一个简单的问题,但答案却非常复杂。简短的答案是您可以使用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”,但这只是语言设计人员为保存类型而送的礼物。相反,您必须指定成员函数为静态,才能选择退出该成员函数,以自动获取额外的“ 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,这种混淆就是为什么所有成员函数都需要一个“自我”参数的原因)。

解决此问题的详细方法是创建一个特殊的对象,该对象持有指向所需实例的指针,并具有一个名为“ 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 ::功能对原始指针,在计算器上的记录在这里。它的实现方式超出了此答案的范围,尽管它也很有趣。

不必担心这看起来多么难以置信-您的问题与C ++的几个较暗的角落相交,对boost::bind您的学习非常有用。

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


2
这是一个很好的答案!
aardvarkk

5
在答案的开头,建议将std :: bind1st作为实现该解决方案的一种方法,但是答案的后半部分专门针对boost :: bind。如何使用std :: bind1st?
mabraham

1
@mabraham好,我添加了一个简单的示例,尽管它不能完全解决问题(VS2008):stackoverflow.com/a/45525074/4566599
Roi Danton

3
这应该是公认的答案!如果您无法更改库或传递可选的args,则接受的答案实际上不起作用。
卡森·麦克尼尔

1
@Joseph Garvin:无法看到std :: bind的答案。这要求参数的类型为std :: function,而不是普通的C函数指针。只是因为您隐藏了传递此内容,这并没有比接受的答案更好。好吧,好的,如果您可以在源代码级访问该函数签名,则可以将foo *更改为std :: function <foo_signature>,然后只需更改此代码即可,前提是所有编译器均已更新至C ++ 11 ,但是如果您没有源访问权限,那么您就是F * ED,因为签名不兼容。这与C ++ lambda表达式相同。
Stefan Steiger,

19

该答案是对以上评论的答复,不适用于Visual Studio 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");Init如果f具有字符串参数,则可以使用in )。


1
非常感谢您的回答!我已经花了两个多小时来搜索和尝试不同的方法,但实际上没有任何效果。但这很简单,一分钟后就可以了。
BadK '18 -10-2

3

需要什么论点Init?什么是新的错误消息?

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


3

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

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


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

死灵法师。
我认为到目前为止的答案还不清楚。

让我们举个例子:

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

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

现在您要生成一个PNG。为此,您可以将函数调用为Jpeg

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 *);

1

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

DEFINE接口:

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;
    }
};

0

这个问题和答案C ++ FAQ精简版涵盖了你的问题,涉及的答案相当不错,我认为的考虑。我链接的网页的简短摘要:

别。

因为没有成员调用成员函数就没有意义,所以您不能直接执行此操作(如果X窗口系统是用C ++重写的,则它可能会传递对对象的引用,而不仅仅是传递指向函数的指针;很自然地,对象将体现所需的功能,甚至可能更多)。


1
现在的链接是isocpp.org/wiki/faq/pointers-to-members#memfnptr-vs-fnptr ; 看起来他现在说“不要”。这就是仅链接的答案不好的原因。
有限赎罪

2
我已经修改了答案@LimitedAtonement。感谢您指出了这一点。您完全正确,只有链接的答案是低质量的答案。但是我们不知道早在2008年:-P
Onorio Catenacci

0

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

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

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

CLoggersInfra* pLoggerInfra;

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

如果Init 可以更改API ,则有很多选择-对象非静态成员函数指针,函数对象std::function或接口函数。

有关C ++工作示例的不同变化,请参见有关回调的文章。


0

看起来std::mem_fn(C ++ 11)确实可以满足您的需求:

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.