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

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

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);


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


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


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

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

我正在使用VS2008。

谢谢!

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


评论


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

– Stefan Steiger
19-3-27在10:29



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

– Johannes Schaub-小人
19-3-27在10:37



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

– Stefan Steiger
19-3-27在16:54



#2 楼

这是一个简单的问题,但答案却非常复杂。简短的答案是您可以使用std::bind1stboost::bind做您想做的事情。下面是更长的答案。
编译器建议您使用&CLoggersInfra::RedundencyManagerCallBack是正确的。首先,如果RedundencyManagerCallBack是成员函数,则该函数本身不属于CLoggersInfra类的任何特定实例。它属于类本身。如果您曾经调用过静态类函数,则可能已经注意到您使用了相同的SomeClass::SomeMemberFunction语法。由于函数本身属于类而不是特定实例,因此它本身是“静态的”,因此您使用相同的语法。 “&”是必要的,因为从技术上讲您不会直接传递函数-函数不是C ++中的真实对象。相反,从技术上讲,您正在传递函数的内存地址,即,指向函数指令在内存中开始的位置的指针。结果是一样的,您实际上是在“传递一个函数”作为参数。
但这只是本例中问题的一半。就像我说的那样,该函数不“属于”任何特定实例。但这听起来像您想将其作为回调传递时考虑到特定的实例。要了解如何执行此操作,您需要了解真正的成员函数:带有额外隐藏参数的常规未定义任何类函数。
例如:
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!

RedundencyManagerCallBack需要几个参数?通常我们会说1.。但实际上,foo确实需要2.看看A :: foo的定义,它需要A的特定实例,以使“ this”指针有意义(编译器需要知道什么“这是)。通常,通过语法A::foo来指定您希望“此”的方式。但这只是将MyObject.MyMemberFunction()的地址作为第一个参数传递给MyObject的语法糖。类似地,当我们在类定义中声明成员函数时,我们不会在参数列表中放入“ 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'想要一个指向带有一个参数的函数的指针。但是MyMemberFunction是指向带有两个参数的函数的指针,这两个参数是常规参数和秘密的“ this”参数。这就是为什么仍然出现编译器错误的原因(请注意:如果您曾经使用过Python,这种混淆就是为什么所有成员函数都需要一个“自我”参数的原因)。
详细的方法解决这个问题的方法是创建一个特殊的对象,该对象持有指向您想要的实例的指针,并具有一个称为“运行”或“执行”(或重载“()”运算符)的成员函数,该函数采用成员的参数函数,只需在存储的实例上使用这些参数调用成员函数。但这需要您更改“ Init”以使用特殊对象而不是原始函数指针,而且听起来Init是其他人的代码。并且每次出现此问题时都要创建一个特殊的类,这将导致代码膨胀。
因此,现在,终于有了一个不错的解决方案&CLoggersInfra::RedundencyManagerCallBackboost::bind,每个文档都可以在这里找到:
boost :: bind文档,
boost :: function docs
boost::function将让您接受一个函数,以及该函数的参数,并创建一个新函数,其中该参数被“锁定”到位。因此,如果我有一个将两个整数相加的函数,则可以使用boost::bind来创建一个新函数,其中一个参数被锁定为5。此新函数将仅使用一个整数参数,并且始终将其专门添加5。使用此技术,您可以将“隐藏”的“ this”参数“锁定”为特定的类实例,并生成一个仅接受一个参数的新函数,就像您想要的一样(请注意,隐藏参数始终是第一个参数,和普通参数按顺序排列)。查看boost::bind文档中的示例,他们甚至专门讨论了如何将其用于成员函数。从技术上讲,您也可以使用一个称为boost::bind的标准函数,但是[std::bind1st][3]更为通用。
当然,这里还有一个陷阱。 boost::bind将为您带来一个不错的boost :: function,但是从技术上讲,这仍然不是像Init可能想要的原始函数指针。幸运的是,boost提供了一种将boost :: function转换为原始指针的方法,如此处StackOverflow所述。它的实现方式超出了此答案的范围,尽管它也很有趣。
不用担心这看起来是否太可笑了-您的问题与C ++的多个黑暗角落相交,一旦学习它,boost::bind就会非常有用。
C ++ 11更新:现在,您可以使用捕获“ this”的lambda函数来代替boost::bind。这基本上是让编译器为您生成相同的东西。

评论


这是一个很好的答案!

– aardvarkk
13年10月31日,0:45

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

–马布拉汉姆
2014年6月1日,11:05

@mabraham好的,我添加了一个简单的示例,尽管它并不完全适合问题(VS2008):stackoverflow.com/a/45525074/4566599

–罗伊·丹顿(Roi Danton)
17年8月5日在18:18

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

–卡森·麦克尼尔(Carson McNeil)
17年8月11日在23:10

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

– Stefan Steiger
19 Mar 27 '19在10:39

#3 楼

此答案是对以上评论的答复,不适用于Visual Studio 2008,但应用于较新的编译器。


同时,您不必再使用void指针,并且由于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");中使用Init)。

评论


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

– BadK
18-10-2在12:11

#4 楼

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

C ++中的方法指针很难使用。除了方法指针本身之外,您还需要提供一个实例指针(在您的情况下为this)。也许Init希望将其作为一个单独的参数?

#5 楼

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

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

#6 楼

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


#7 楼

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

举个例子:

假设您有一个像素数组(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的更改。

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

但是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项目。

#8 楼

我可以看到init具有以下替代:

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)


其中CALLBACK_FUNC_EX

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


#9 楼

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

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


#10 楼

我认为C ++ FAQ Lite中的问题和答案涵盖了您的问题以及答案中涉及的注意事项。我链接的网页的简短摘要:


不要。

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


评论


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

–lmat-恢复莫妮卡
16年1月12日在19:45

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

–奥诺里奥(Atorio Catenacci)
16年1月13日在16:04

#11 楼

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

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

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

CLoggersInfra* pLoggerInfra;

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


m_cRedundencyManager->Init(RedundencyManagerCallBackWrapper);


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

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

#12 楼

看起来std::mem_fn(C ++ 11)完全满足您的需求:函数模板std :: mem_fn生成包装器对象,用于指向成员的指针,该指针可以存储,复制和调用对象。指向成员的指针。调用std :: mem_fn时,可以使用对象的引用和指针(包括智能指针)。