如何将成员函数传递到期望自由函数的位置?

187

以下是问题的内容:考虑下面这段代码:

#include <iostream>


class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main()
{
    aClass a;

    function1(&test);
    function1(&aClass::aTest); // <-- How should I point to a's aClass::test function?
}

我该如何将aaClass::test作为参数传递给function1? 我想要访问类的成员。


2
请查看这个答案https://dev59.com/IHE95IYBdhLWcg3wOLU1以及这个C++ FAQ http://www.parashift.com/c++-faq/pointers-to-members.html - amdn
17
这绝对不是重复的问题(至少不是与链接的特定问题重复)。那个问题是关于如何声明一个指向函数的成员;这个问题是关于如何将非静态成员函数的指针作为参数传递。 - CarLuva
9个回答

190

使用函数指针没有任何问题。但是,非静态成员函数的指针不像普通函数指针:成员函数需要在传递给函数的对象上调用。因此,您上面的成员函数的签名为:

void (aClass::*)(int, int)

与您尝试使用的类型不同

void (*)(int, int)

一种方法是将成员函数设为 static,这样它就不需要在对象上被调用,可以使用类型 void (*)(int, int) 来调用。
如果您需要访问类的任何非静态成员,并且您需要坚持使用函数指针(例如,因为该函数是 C 接口的一部分),则最好始终向函数指针传递 void*,并通过转发函数调用成员。 转发函数从 void* 获取对象,然后调用成员函数。
在正确的 C++ 接口中,您可能需要查看使函数采用模板化函数对象参数的方式,以使用任意类类型。 如果不想使用模板化接口,则应使用类似于 std::function<void(int, int)> 的东西:可以为这些创建一个适当的可调用函数对象,例如使用 std::bind()
使用模板类型参数或适当的 std::function<...> 的类型安全方法比使用 void* 接口更可取,因为它们消除了由于将类型转换为错误类型而导致的潜在错误。
为了阐明如何使用函数指针来调用成员函数,以下是一个示例:
// the function using the function pointers:
void somefunction(void (*fptr)(void*, int, int), void* context) {
    fptr(context, 17, 42);
}

void non_member(void*, int i0, int i1) {
    std::cout << "I don't need any context! i0=" << i0 << " i1=" << i1 << "\n";
}

struct foo {
    void member(int i0, int i1) {
        std::cout << "member function: this=" << this << " i0=" << i0 << " i1=" << i1 << "\n";
    }
};

void forwarder(void* context, int i0, int i1) {
    static_cast<foo*>(context)->member(i0, i1);
}

int main() {
    somefunction(&non_member, nullptr);
    foo object;
    somefunction(&forwarder, &object);
}

3
我不喜欢这个答案,因为它使用了 void*,这意味着你可能会遇到非常严重的 bug,因为它不再经过类型检查。 - Superlokkus
@Superlokkus:你能否请我们指出另一种选择? - Dietmar Kühl
请查看PeterBecker和特别是@MattPhillips的下面的答案,DietmarKühl。 - Superlokkus
@Superlokkus:请注意,我的答案实际上提到了这两种方法!它只是详细解释了如何调用成员函数来执行此操作,因为这并不直接明显。 - Dietmar Kühl
1
@Evgeny:如果member是指向成员的指针,那么object.*member就是对应成员类型的引用。然而,如果member是指向成员函数的指针,object.*member只能作为调用表达式的一部分使用,并且它本身并不存在(请参见Compiler Explorer演示)。从概念上讲,它将是具有绑定第一个参数的成员函数的版本。您可以使用std::bind(...)创建类似的东西,但这不是一种语言级别的概念。 - Dietmar Kühl
显示剩余9条评论

138

@Pete Becker的回答很好,但在C++11中,您还可以不将class实例作为显式参数传递给function1:

#include <functional>
using namespace std::placeholders;

void function1(std::function<void(int, int)> fun)
{
    fun(1, 1);
}

int main (int argc, const char * argv[])
{
   ...

   aClass a;
   auto fp = std::bind(&aClass::test, a, _1, _2);
   function1(fp);

   return 0;
}

1
void function1(std::function<void(int, int)>) 正确吗? - Deqing
4
在函数参数中需要给定一个变量名,然后这个变量名就是实际传递的参数。例如:void function1(std::function<void(int, int)> functionToCall) 然后使用 functionToCall(1, 1); 来调用该函数。我曾经试图编辑答案,但不知何故被拒绝了,不过或许它会在某个时候得到投票支持。 - Dorky Engineer
1
@DorkyEngineer 这很奇怪,我认为你一定是对的,但我不知道这个错误怎么会被忽视那么长时间。无论如何,我现在已经编辑了答案。 - Matt Phillips
4
我发现了这篇文章,说 std::function 会带来严重的性能惩罚。 - kevin
3
@Kevin,你可能需要修改你的评论,因为那篇帖子的回答显示出基准测试中的一个缺陷。 - Jorge Leitao
1
非常好的答案 - 这正是我在寻找的。不过有一个小细节 - 我认为应该是:auto fp = std::bind(&aClass::test, &a, _1, _2); 如果您不将类实例作为引用传递,那么编译器(至少是MSVC2019)似乎会尝试移动它,这可能不是您想要的。另请参见此处的示例(auto f3 = ...):https://en.cppreference.com/w/cpp/utility/functional/bind。 - James S.

66

成员函数指针和普通函数指针不同,使用成员函数指针需要一个指向该函数的指针(显然)和一个对象来执行它。因此,适当的function1版本应为:

void function1(void (aClass::*function)(int, int), aClass& a) {
    (a.*function)(1, 1);
}

并调用它:

aClass a; // note: no parentheses; with parentheses it's a function declaration
function1(&aClass::test, a);

1
非常感谢。我刚刚发现这里括号很重要:function1(&(aClass::test), a)在MSVC2013中可以工作,但在gcc中不行。gcc需要&直接放在类名前面(我觉得这很令人困惑,因为&运算符是获取函数的地址,而不是类的地址)。 - Markus Sabin
1
我曾认为 void (aClass::*function)(int, int) 中的 function 是一种类型,因为在 typedef void (aClass::*function)(int, int) 中它是一种类型。 - Olumide
1
@Olumide — typedef int X; 定义了一种类型;int X; 创建了一个对象。 - Pete Becker

16

自2011年以来,如果您可以更改function1,请这样做:

#include <functional>
#include <cstdio>

using namespace std;

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

template <typename Callable>
void function1(Callable f)
{
    f(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main()
{
    aClass obj;

    // Free function
    function1(&test);

    // Bound member function
    using namespace std::placeholders;
    function1(std::bind(&aClass::aTest, obj, _1, _2));

    // Lambda
    function1([&](int a, int b) {
        obj.aTest(a, b);
    });
}

(演示版本)

同时请注意,我修复了您破损的对象定义(aClass a(); 声明了一个函数)。


7
重要的是要注意,除非你可以更改接收函数的代码签名,否则没有(简单的)方法来实现这一点。这将尝试在不具有与函数相同的闭包的语言中实现闭包(C++ 中闭包的签名不同)。
有两种实际的方法可以实现这一点:
1. 使用某种单例/全局变量来存储闭包,然后传递一个帮助函数,使用该闭包调用所需的函数。以下是一个示例: 2. 在需要闭包时创建一个新的可调用对象,并将其传递给需要它的代码。
#include <stdio.h>

template<class C, typename ReturnType, typename... Args>
class ClosureSingleton {
    typedef ReturnType (C::*FuncType)(Args...);

   public:
    static ClosureSingleton& getInstance() {
        static ClosureSingleton instance;
        return instance;
    }

    void setClosure(C* obj, FuncType f) {
        this->obj      = obj;
        this->function = f;
    }
    static ReturnType funcPtr(Args... args) {
        C* obj    = getInstance().obj;
        auto func = getInstance().function;

        return (obj->*func)(args...);
    }

   private:
    ClosureSingleton() {}

    C* obj;
    FuncType function;

   public:
    ClosureSingleton(ClosureSingleton const&) = delete;
    void operator=(ClosureSingleton const&)   = delete;
};

class aClass {
   public:
    void aTest1(int a, int b) { printf("%d + %d = %d\n", a, b, a + b); }
    int aTest2(int a, int b) { return a + b; }
};

void function1(void (*function)(int, int)) {
    function(1, 1);
}
int function2(int (*function)(int, int)) {
    return function(1, 1);
}

int main() {
    aClass tmp;
    ClosureSingleton<aClass, void, int, int>::getInstance().setClosure(
        &tmp, &aClass::aTest1);
    function1(&ClosureSingleton<aClass, void, int, int>::funcPtr);

    ClosureSingleton<aClass, int, int, int>::getInstance().setClosure(
        &tmp, &aClass::aTest2);
    printf(
        "function2: %d\n",
        function2(&ClosureSingleton<aClass, int, int, int>::funcPtr));

    return 0;
}

当然,这种方法的明显缺点是需要在每次调用之前设置闭包,并且存在一些线程安全问题。虽然不理想,但在特定情况下可能可行。
  1. 使用像asmjit或动态编译这样的东西来动态编译并传递函数到C代码中。这仅适用于允许将堆段标记为可执行的机器。由于要编写汇编代码来实现这一点,因此非常不可移植。但是,如果您使其工作,那么您确实会拥有真正的闭包,尽管与大多数编程语言实现闭包的方式相比,创建闭包的成本要高得多(它们不会重复函数组装体,而是使用上下文对象)。
  2. 修补具有函数处理程序的lib / dll以更改其签名以允许上下文对象。同样,这是一个非常脆弱和不优化的解决方案。

我的原始答案,它并没有真正回答问题,但人们发现它很有用:

不确定为什么这个极其简单的解决方案被忽略了:

#include <stdio.h>

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

template<class C>
void function1(void (C::*function)(int, int), C& c)
{
    (c.*function)(1, 1);
}
void function1(void (*function)(int, int)) {
  function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(&test);
    function1<aClass>(&aClass::aTest, a);
    return 0;
}

输出:

1 - 1 = 0
1 + 1 = 2

7

我曾经问过类似的问题(C++ openframeworks passing void from other classes),但是我找到的答案更清晰,所以在这里解释一下以备将来参考:

使用std::function更容易实现,代码如下:

 void draw(int grid, std::function<void()> element)

然后调用如下:

 grid.draw(12, std::bind(&BarrettaClass::draw, a, std::placeholders::_1));

甚至更简单:
  grid.draw(12, [&]{a.draw()});

您需要创建一个lambda,通过引用捕获该对象并调用它。


1
考虑到这是标记为CPP的,我认为这是最干净的解决方案。我们可以在draw中使用对std::function的常量引用,而不是在每次调用时复制它。 - Sohaib
2
在这个例子中不应该使用占位符,因为该函数不接受任何参数。 - kroiz
请注意,从C++14开始,应优先选择lambda而不是std :: bind,理由如下(例如参见《Effective Modern C ++》第34项:将lambda表达式作为std :: bind的首选项)。 - Bim

0

我将成员函数设为静态的,现在一切正常:

#include <iostream>

class aClass
{
public:
    static void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

void function1(int a,int b,void function(int, int))
{
    function(a, b);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(10,12,test);
    function1(10,12,a.aTest); // <-- How should I point to a's aClass::test function?

    getchar();return 0;
}

不仅解决了主要问题“如何将a的aClass :: test用作function1的参数?”,而且建议避免使用类外部的代码修改类的私有变量。 - mathengineer

0

如果你实际上不需要使用实例a(即你可以像@mathengineer的答案一样将其静态化),那么你可以简单地传入一个非捕获lambda表达式。(它会衰减为函数指针)


#include <iostream>

class aClass
{
public:
   void aTest(int a, int b)
   {
      printf("%d + %d = %d", a, b, a + b);
   }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

int main()
{
   //note: you don't need the `+`
   function1(+[](int a,int b){return aClass{}.aTest(a,b);}); 
}

Wandbox

的翻译内容为:

Wandbox


注意:如果aClass的构造成本高或具有副作用,则这可能不是一个好方法。


-3
你们可以停止苦思冥想了。这里提供了一个包装器,以支持接受普通 C 函数作为参数的现有函数的成员函数。 thread_local 指令是关键所在。

http://cpp.sh/9jhk3

// Example program
#include <iostream>
#include <string>

using namespace std;

typedef int FooCooker_ (int);

// Existing function
extern "C" void cook_10_foo (FooCooker_ FooCooker) {
    cout << "Cooking 10 Foo ..." << endl;
    cout << "FooCooker:" << endl;
    FooCooker (10);
}

struct Bar_ {
    Bar_ (int Foo = 0) : Foo (Foo) {};
    int cook (int Foo) {
        cout << "This Bar got " << this->Foo << endl;
        if (this->Foo >= Foo) {
            this->Foo -= Foo;
            cout << Foo << " cooked" << endl;
            return Foo;
        } else {
            cout << "Can't cook " <<  Foo << endl;
            return 0;
        }
    }
    int Foo = 0;
};

// Each Bar_ object and a member function need to define
// their own wrapper with a global thread_local object ptr
// to be called as a plain C function.
thread_local static Bar_* Bar1Ptr = NULL;
static int cook_in_Bar1 (int Foo) {
    return Bar1Ptr->cook (Foo);
}

thread_local static Bar_* Bar2Ptr = NULL;
static int cook_in_Bar2 (int Foo) {
    return Bar2Ptr->cook (Foo);
}

int main () {
  Bar1Ptr = new Bar_ (20);
  cook_10_foo (cook_in_Bar1);
  
  Bar2Ptr = new Bar_ (40);
  cook_10_foo (cook_in_Bar2);
  
  delete Bar1Ptr;
  delete Bar2Ptr;
  return 0;
}

请评论此方法的任何问题。
其他答案未能调用现有的纯 C 函数:http://cpp.sh/8exun

7
所以,与使用std::bind或lambda包装实例不同,您依赖于全局变量。我无法看出这种方法相比其他答案有任何优势。 - super
@super,其他答案不能调用以普通C函数作为参数的现有函数。 - Necktwi
5
问题是如何调用成员函数。在问题中,传递一个自由函数已经可以使用。您也没有传递任何内容。这里只有硬编码的函数和全局Foo_指针。如果要调用不同的成员函数,该怎么办?您将不得不重写基础函数,或者为每个目标使用不同的函数。 - super

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