如何将成员函数指针传递给接受常规函数指针的函数?

4

我有一个播放器类,看起来像这样(只保留了解决此问题所需的部分):

class Player
{
    public:
        Player();
        ~Player();

        void kill();
        void death();
        void reset();
};

kill()death()reset() 函数如下所示:

void Player::kill()
{
    void (*dPtr)() = &death;

    Game::idle(dPtr, 48);
}

void Player::death()
{
    reset();
}

void Player::reset()
{
    //resets
}

idle函数是Game的一个静态成员函数,它接受一个函数指针和一个整数n,并在n个tick之后调用该函数。以下是函数内容,实现细节不重要:

class Game {
    static void idle(void (*)(), int);
};

这段代码给了我一个错误:
ISO C++ forbids taking the address of an unqualified or parenthesized non-static member function to form a pointer to member function.  Say '&Player::death' [-fpermissive]

所以我将这一行改为:
    void (*dPtr)() = &death;

to

    void (Player::*dPtr)() = &Player::death;

为了解决这个问题,我需要修改我的空闲函数调用。但是,由于它需要一个常规函数指针,而我传递的是成员函数指针,因此会导致错误:
no matching function for call to 'Game::idle(void (Player::*&)(), int)'

我的问题是: 如何将成员函数指针 Player::*dPtr 传递给以 void (*)() 作为参数的空闲函数?

或者是否有其他方法可以解决我之前遇到的错误,该错误禁止我从未经修饰的成员函数取地址以形成成员函数指针?


请查看boost::bindboost::function - ezaquarii
使用std::bind(),当前标准中不需要boost。 - πάντα ῥεῖ
@zwol,那么函数static void idle(void (*)(), int);需要更改。问题出在哪里? - πάντα ῥεῖ
你能改变idle的界面吗? - MikeMB
@Yaxlat 不,它不会。这就是你的整个问题所在。 - zwol
显示剩余4条评论
4个回答

5
另一个答案提到需要两个指针,但是C++已经有容器可以做到这一点了,所以使用这些容器会使你的代码更简单。(在C++03中,下面的一些std::项是std::tr1::)。
示例代码:
#include <iostream>
#include <functional>

struct Game 
{ 
    static void idle( std::function<void()> func, int x )
        { std::cout << "x = " << x << "\n"; func(); }
};

struct Player
{
     void death() { std::cout << "player.death\n"; }
     void kill() { Game::idle( std::bind(&Player::death, this), 48 ); }
};

int main()
{
    Player p;
    p.kill();
}

生命周期注意事项: std::bind 按值绑定。使用 *this 意味着会复制一个 Player 并将其存储在 std::function 对象中,根据需要复制它。

使用 this 意味着函数对象会存储一个指针,因此如果你实际上将函数对象存储在 Game::idle 中,必须确保在从 Game::idle 的列表中删除该函数对象之前不会销毁此 Player


你能谈一下这种方法的生命周期问题吗?具体来说,假设Game::idle将其参数放入队列中以便在“稍后”执行,远在它和Player::kill都返回之后。显然,Player对象必须在那时存活。将std::functionstd::bind返回的未指定对象视为值类型,与生命周期无关,这样安全吗?有没有办法使std::function持有对传递给std::bind的某些或所有参数的shared_ptr - zwol
@zwol编辑了帖子以提及生命周期问题。使用shared_ptrbind是可能的,但前提是您的原始对象也由shared_ptr管理。 - M.M
使用延迟空闲和shared_ptr的示例代码(http://ideone.com/ba9Y7Y)。为了强制在shared_ptr下创建“Player”,您可以拥有私有构造函数和友元“Game”,并给“Game”一个工厂函数。 - M.M

3

如果通过指针调用成员函数,您需要 两个 指针:指向函数本身的指针和指向要使用的对象的指针 this。您的 Game::idle API 不支持这种用法。您需要更改它,以便将至少一个参数(约定俗称为类型 void *)传递给回调。然后可以使用以下模式:

struct Player
{
        // ...
        void kill();

        // ...
        static void call_kill(void *self);
};

void Player::call_kill(void *self)
{
    static_cast<Player *>(self)->kill();
}

struct Game
{
    static void idle(void (*)(void *), void *, int);
};

void Game::idle(void (*callback)(void *), void *arg, int ticks)
{
    // ...
    callback(arg);
    // ...
}

void kill_player_delayed(Player *p, int ticks)
{
    Game::idle(Player::call_kill, static_cast<void *>(p), ticks);
}

你需要为每个实例方法X编写一个静态的call_X方法来调用它。
另一种方法是更符合C++风格和灵活性,代码量较少,但运行时成本较高(每次调用需要三次间接函数调用和堆分配-释放循环,而不是单个间接函数调用)。在这种方法中,Game::idle需要使用特定类的对象,并具有虚回调方法。然后给该类一个模板子类,可以调用任何实现operator()的东西,例如std::bind的结果。
struct Runnable { virtual ~Runnable(); virtual void invoke() = 0; };

template <typename T> struct TRunnable : Runnable {
    TRunnable(T target) : target(target) {}
    void invoke() { target(); }
private:
    T target;
};
template <typename T> TRunnable<T>* make_Runnable(T obj)
{ return new TRunnable<T>(obj); }

struct Game
{
    static void idle(Runnable *, int);
};

void Game::idle(Runnable *r, int ticks)
{
    // ...
    r->invoke();
    delete r;
    // ...
}

struct Player
{
    // ...
    void kill();
    // ...
};

void kill_player_delayed(Player *p, int ticks)
{
    Game::idle(make_Runnable(std::bind(&Player::kill, p)), ticks);
}

您不能直接将std::bind的结果作为参数传递给Game::idle,因为该对象的类型未指定(并且取决于您调用std::bind的方式),因此它只能用作模板函数调用的参数。通过调用适配器类的虚方法来是Game::idle在编译时保持离线,并仍然可以使用绑定调用对象。

无论采用哪种方法,都要注意对象生命周期问题。特别是,如果Game::idle在返回之前没有调用其回调函数,则需要确保原始对象和(在第二种方法中)make_Runnable返回的对象存活到回调触发。这就是为什么make_Runnable使用new的原因。


我不完全理解你的第一种方法。什么是kill_player_delayed函数?我假设它指的是我放置的kill函数,而你的kill()指的是我的death()吗? - Yaxlat
@Yaxlat 抱歉,我误读了您原始的示例。我以为您没有展示调用Game :: idle的完整函数,并且killdeath都可能需要通过Game :: idle调用。所以我想出了kill_player_delayed来调用Game::idle - zwol
这是一个非常好的方法...实际上非常好,以至于标准库已经包含了它,名称为std::function!(重新发明可能不是那么好) - Ben Voigt
@BenVoigt 很遗憾,我几乎所有的C ++经验都是在尘封的代码库中完成的,这些代码库甚至无法使用C ++98标准库,更别提C ++11了,所以我只有一个大概的想法。你在另一个答案中使用std :: function,我会给它投票;-) - zwol

1
因为我真的不喜欢将void*转换为其他对象的答案(在C ++中几乎从不需要!),而且没有人使用评论中的建议发布了答案,所以我要建议这样做。
为您的回调使用模板类型!
像这样:

像这样:

class Game{
    template<typename Func>
    static void idle(Func &&func, int i){
        // game stuff
        func();
        // other game stuff
    }
};

那么你就不会失去所有的类型安全性(强制转换 void*),而且这应该是最快的解决方案。


另外,在分配函数指针时,您可以改变代码以使其更易读:

void Player::kill(){
    Game::idle([this](){this->death();}, 48);
}

这比编写正确的函数指针类型要好得多。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Ben Voigt
@BenVoigt 我总是避免在任何高性能编程(如游戏编程)中使用virtual函数或std::function,因为间接调用会导致性能下降。但如果我需要存储回调,你是完全正确的,重新发明轮子对任何人都没有用! - RamblingMad

0

你不能这样做,因为指向[静态]函数的指针是一个大小为void* 的单一指针。相反,成员函数需要更多信息,例如两个指针:一个用于this,另一个用于函数本身,因此成员函数指针具有sizeof > sizeof (void*)

因此,你有两个选择:

  1. 将你的idle()函数签名更改为void idle(void (*)(), void*, int);,这样你就能以某种方式传递this
  2. 或者创建一个静态变量来保存this指针。但这假设在任何给定时刻只有一个death()可以在空闲队列中。

通常情况下,人们选择1)。


是的,最终我采用了zwol的建议,使用了方法1。我错误地认为C++函数指针包括它们所属的对象的指针,当它们是成员函数时。 - Yaxlat

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