C++成员函数指针转全局函数指针

3

我需要解决一个在C++中棘手的问题。有一个dll,我不能修改它。它接受一个函数指针作为参数。如果我传递一个指向全局函数的指针,一切都正常。不幸的是,有一组相同类对象需要传递给dll。在C#中,我通过使用委托解决了这个问题。在C++中该如何实现?使用std::function并不起作用。使用它会导致运行时出现编码约定错误。最好使用MSVC2010。

#include <stdio.h>


// global function which works
void __stdcall task_global(float x, float y) { printf("Called global function with: %f %f\n", x, y); }
typedef void(__stdcall *f_pointer)(float, float);



// try of using a member function
class BaseTask {
public:
    virtual void __stdcall task(float x, float y) = 0;
};


class ListeningTask :public BaseTask {
public:
    void __stdcall task(float x, float y) { printf("Called this member function with: %f %f\n", x, y); }
};

typedef void (BaseTask::*member_f_pointer)(float, float);



// the dll to use
class RegisterTask {
public:
    // no posibility to access or modify!
    void __stdcall subscribe(f_pointer fp) { fp(1.0f, 2.0f); }
    // just for demonstration how to use a member function pointer
    void __stdcall subscribeMemberDemo(member_f_pointer mfp) {  /*how to use mfp?*/};
};



int main() {

    RegisterTask register_task{};

    // use global function
    f_pointer pointer_to_global_task = task_global;
    register_task.subscribe(pointer_to_global_task);


    /*---------------------------------------------------------------*/
    // use member function?
    std::list<ListeningTask> listening_task_list;
    for(int i = 0; i < 10; i++) {
        listening_task_list.push_back(ListeningTask lt);
        member_f_pointer pointer_to_member_task = &listening_task_list.back().task; //error C2276: '&': illegal operation on bound member function expression
        register_task.subscribeMemberDemo(pointer_to_member_task);

        // the tricky and important one to solve
        // how to pass the member function to this subscribe(f_pointer)?
        register_task.subscribe(pointer_to_member_task);
    }

    getchar();
    return 0;
}

重要的问题是如何将成员函数指针传递给RegisterTask::subscribe(f_pointer)
括号内的问题是如何将成员函数传递给RegisterTask::subscribeMemberDemo(member_f_pointer)
我希望有人可以帮助我解决这个问题,我已经工作了好几天了。
编辑:我修改了问题以强调ListenerTask列表的问题。如何传递成员函数指针现在已经通过@pokey909和@AndyG的回答变得清晰明了。他们都提供一个对象或一组对象的指针。如果调用回调,则会同时调用一个ListenerTask或所有std::list<*ListenerTask>。但如何让列表中的仅一个ListenerTask被调用呢?传递多个回调到dll似乎可行。因为以下使用全局函数的示例可以正常工作。
void __stdcall task_global_1(float x, float y) { printf("Called global function 1 with: %f %f\n", x, y); }
void __stdcall task_global_2(float x, float y) { printf("Called global function 2 with: %f %f\n", x, y); }
void __stdcall task_global_3(float x, float y) { printf("Called global function 3 with: %f %f\n", x, y); }

typedef void(__stdcall *f_pointer)(float, float);

int main() {
    // give the functions to the dll.
    f_pointer pointer_to_global_task_1 = task_global_1;
    register_task.subscribe(pointer_to_global_task_1);

    f_pointer pointer_to_global_task_2 = task_global_2;
    register_task.subscribe(pointer_to_global_task_2);

    f_pointer pointer_to_global_task_3 = task_global_3;
    register_task.subscribe(pointer_to_global_task_3);
}

有三个全局函数指针。它们都被提供给dll。现在,如果dll有一个task_global_2任务,它只会通知这个!如何通过成员函数指针实现这种区分?

注意: 我得到了dll的源代码。希望这可以帮助解决问题。不幸的是,修改和构建是不可能的。这里是回调定义

type TCallback = procedure( x : single; y : single; ); stdcall;

procedure subscribe(aCallback: TCallback ); StdCall;
begin
  TaskSocket.addTask( aCallback );
end;

procedure TSocket.addTask( aCallback : TCallback);
var newTask : TTask;
begin
  newTask := TTask.Create(aCallback);
  TaskList.addItem(newTask);
end;

1
你做不到。隐藏的 this 参数会让你无从下手。相反,将调用你想要执行的方法的静态方法传递到 dll 中。你需要找到一些方法来知道在静态方法中调用哪个对象。如果你只有一个对象实例,那么这将很容易。如果不是... - user4581301
2
为什么 RegisterTask::subscribe 没有声明为静态的?它并没有使用 this - Barmar
1
你已经抽象化了回调函数指针的细节,我怀疑。如果你还没有提供确切的签名,请提供。重要的不是你认为的,而是传入的函数指针的确切签名。以及接受它的函数。(最小化是好的,但很难!) - Yakk - Adam Nevraumont
@Yakk 根据他发布的内容,回调函数只需接受两个 float 类型参数。因此,没有 void * 参数可以保存对象的指针。 - Barmar
@Barmar 我认为这里必须有一个不透明指针。因为我可以在 C# 中使用此 DLL,使用像 public delegate void TaskDelegate(float x, float y); 这样的委托来指向对象。 - solarisx
显示剩余6条评论
2个回答

3
您可以使用一个独立的函数,调用一个包装器来将您的实例绑定到它上面。以下是一个简单的示例:
#include <iostream>
#include <string>
#include <functional>

// global function which works
std::function<void(float, float)> memberCb;
void task_global(float x, float y) { memberCb(x, y); }
typedef void(*f_pointer)(float, float);


// try of using a member function
class BaseTask {
public:
    virtual void  task(float x, float y) = 0;
};


class ListeningTask :public BaseTask {
public:
    void  task(float x, float y) { printf("Called this member function with: %f %f\n", x, y); }
};

typedef void (BaseTask::*member_f_pointer)(float, float);

void callbackWrapper(BaseTask* t, float x, float y) { t->task(x, y); }

// the dll to use
class RegisterTask {
public:
    // no posibility to access or modify!
    void  subscribe(f_pointer fp) { 
        fp(1.0f, 2.0f); 
    }
    // just for demonstration how to use a member function pointer
    void  subscribeMemberDemo(member_f_pointer mfp) {  /*???*/ };
};



int main() {

    RegisterTask register_task{};

    ListeningTask listening_task{};
    memberCb = std::bind(&callbackWrapper, &listening_task, std::placeholders::_1, std::placeholders::_2);
    register_task.subscribe(task_global);

    return 0;
}

注意

根据评论,我不确定这些内容是否在MSVC2010中都能正常工作,因为我没有这个编译器版本。但是基本的C++11支持应该已经包含在其中。

编辑

我不确定这是否能解决你的问题,但这是否符合你的需求?

void callbackWrapper(const std::list<BaseTask*> &taskList, float x, float y) {
    for (auto t : taskList)
        t->task(x, y); 
}

int main() {

    RegisterTask register_task{};

    std::list<BaseTask*> taskList;
    for (int i = 0; i < 4; ++i)
        taskList.push_back(new ListeningTask);
    memberCb = std::bind(&callbackWrapper, taskList, std::placeholders::_1, std::placeholders::_2);
    register_task.subscribe(task_global);

    return 0;
}

编辑2 好的,我想我明白你想要什么了。在不用手动在你的代码中加入全局函数的情况下,最好的解决方案是使用模板魔法。 需要注意的是,这种方法可能没有你想象中的灵活,因为你必须在编译时绑定这些方法。 如果你需要在运行时添加它们,你可能可以使用相同的技巧,但不是模板。只需将所有的std::function对象放入一个向量中,并将其包装在单例模式或类似的东西中。

#include <iostream>
#include <string>
#include <functional>
#include <list>


/* Simulated DLL */
typedef void(*f_pointer)(float, float);
class RegisterTask {
public:
    void  subscribe(f_pointer fp) {
        fp(1.0f, 2.0f);
    }
};



/* Static function generator to ease the pain to define all of them manually */
template<unsigned int T>
std::function<void(float, float)> &getWrapper() {
    static std::function<void(float, float)> fnc;
    return fnc;
}

/* Same here */
template<unsigned int T>
void task_global(float x, float y) { getWrapper<T>()(x, y); }



class BaseTask {
public:
    virtual void  task(float x, float y) = 0;
};
class ListeningTask :public BaseTask {
public:
    ListeningTask(int taskNum) : m_taskNum(taskNum) {}
    void  task(float x, float y) { printf("Called this member of task %d function with: %f %f\n", getTaskNum(), x, y); }
    int getTaskNum() const { return m_taskNum; }
private:
    int m_taskNum;
};


/* Context injector */
void callbackWrapper(BaseTask* t, float x, float y) {
    t->task(x, y);
}

/* Convenience function to bind an instance to a task */
template<unsigned int T>
void bindTask(ListeningTask* t) {
    getWrapper<T>() = std::bind(&callbackWrapper, t, std::placeholders::_1, std::placeholders::_2);
}

int main() {

    RegisterTask register_task{};

    auto task0 = new ListeningTask(1337);
    auto task1 = new ListeningTask(1984);
    auto task2 = new ListeningTask(42);

    bindTask<0>(task0);
    register_task.subscribe(task_global<0>);

    bindTask<1>(task1);
    register_task.subscribe(task_global<1>);

    bindTask<2>(task2);
    register_task.subscribe(task_global<2>);

    return 0;
}

运行代码演示


我以为他说他正在使用一个早于std::bind的C++版本。 - Barmar
刚刚测试了一下,编译并运行成功。只需要在变量声明中去掉 {} 即可。 - user4581301
@pokey909 谢谢,这个方法很好用。但是它并没有完全解决我的问题,因为memberCbtask_global是全局的,我只能将一个Task对象绑定到它们上面。那么如果我使用std::list<ListenerTask>该怎么办呢? - solarisx
@pokey909 再次感谢您的广泛帮助!我不知道编译时ListenerTask的数量,而且您的代码不能处理动态数量。有趣的是这个问题在C++中似乎无法解决,但在C#中使用委托轻松完成。 - solarisx
就像我在上一段中所说的那样:“如果您需要在运行时添加它们,您可能可以使用相同的技巧,但不使用模板。只需将所有std :: function对象放入向量中,并将其包装在单例中即可。” - pokey909
显示剩余6条评论

2

pokey909的答案非常好,但如果你甚至没有访问std::functionstd::bind的权限,我们可以通过黑科技来解决这个问题。

这种方法的要点是我们将定义一个模板类,其中包含到所需函数类型的隐式转换。缺点是每个新的包装器都需要一个新的类型声明。

// assumes two function arguments
template<class Ret, class Mem, class Arg1, class Arg2, int>
struct MemBind
{
    typedef Ret(Mem::*mem_fn_type)(Arg1, Arg2);
    static void Set(mem_fn_type _fn, Mem* _instance)
    {
        fn = _fn;
        instance = _instance;
    }

    static Ret DoTheThing(Arg1 first, Arg2 second)
    {
        return ((*instance).*fn)(first, second);
    }

    typedef Ret(*fn_type)(Arg1, Arg2); 
    operator fn_type ()
    {
        return DoTheThing;
    }

    static mem_fn_type fn;
    static Mem* instance;
};

给定一些结构体 Foo,我们需要的回调函数如下:

struct Foo
{
    void Bar(float a, float b)
    {
        std::cout << "Foo::Bar(float, float) " << a << " , " << b << std::endl;
    }
};

我们需要定义静态成员:

typedef MemBind<void, Foo, float, float, 0> MemBindZero;
template<> Foo* MemBindZero::instance = nullptr;
template<>  void(Foo::*MemBindZero::fn)(float, float)  = nullptr;

我们可以有一个调用者,它接受一个函数指针:

void Caller(void(*_fn)(float, float))
{
    _fn(42.0, 1337.0);
}

关键在于MemBind具有所需函数类型的隐式转换。在MemBindZero的typedef中,0允许我们重复使用相同的类型作为其他参数,但在使用时将计数器递增到1。我认为你可以用__COUNTER__宏或类似的东西来替换它,但这是非标准的,所以我手动完成了这个步骤。
现在的关键是创建一个MemBindZero实例,然后设置static成员,并最终将我们的实例传递给Caller
 Foo f;
 MemBindZero bound;
 bound.Set(&Foo::Bar, &f);
 Caller(bound);

在演示中,我将静态成员初始化封装到一个更方便的宏中:

演示


在演示中,我使用了一个更方便的宏来包装静态成员初始化:

#define MEMBIND(RET, CLASS, ARG1, ARG2, COUNT, ALIAS) \
typedef MemBind<RET, CLASS, ARG1, ARG2, COUNT> ALIAS; \
template<> CLASS * ALIAS::instance = nullptr; \
template<>  RET(CLASS::*ALIAS::fn)(ARG1, ARG2)  = nullptr;

这样我就可以这样调用它:
MEMBIND(void, Foo, float, float, 0, MemBindZero)
MEMBIND(void, OtherFoo, float, float, 1, MemBindOne)

我知道我在这里过于复杂化了。这主要是我在尝试模板。 - AndyG
非常感谢! 这适用于一个回调。 我如何使用 std :: list <Foo> 并将回调链接到其中每个条目? 我扩展了我的问题以更清楚地描述这一点。 也许您可以看一下? - solarisx
我不完全明白您的编辑意图。您想要禁用某些回调吗?或者您是在问如何使订阅者仅调用其中一个回调函数? - AndyG
我希望每个通过subscribe()传递给dll的ListenerTask都有一个回调。就像在我的全局示例中,如果dll需要处理task_global_2,它只会通知这个任务。我需要为每个对象传递一个非静态成员函数给dll,以便dll可以区分传递的函数指针。我还能提供什么其他信息? - solarisx
@solarisx:目前DLL是如何区分回调的?你提到无法修改它,所以我们只能使用它正在使用的任何逻辑。 - AndyG
该DLL获取唯一/不同的函数指针并存储它们。如果可以将ID传递给DLL,然后DLL带有此ID回调,那么这将很容易,但实际上并没有这样做。 - solarisx

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