3

我有一个关于类成员函数的问题。这涉及继承。我在下面编写了以下代码,但我并不真正了解它是如何工作的(我只能猜测):

#include <iostream>

using namespace std;

class Base
{
};

typedef void(Base::*handler)();

#define selector(_SELECTOR) static_cast<handler>(&_SELECTOR)

class Boo : public Base
{
public:
    void callingFunc()
    {
        cout << "Hey there" << endl;
    }

};


class Foo
{   
public:
    void setCallback( Base * instance,  void (Base::*funcToCall)(void) )
    {
        this->instance = instance;
        this->funcToCall = funcToCall;
    }

    void doCall()
    {
        (instance->*funcToCall)();
    }

private:
    Base* instance;
    void (Base::*funcToCall)(void);
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall();  // outputs "Hey there"
}

此代码有效,但我想知道原因的详细信息。doCall()似乎从. funcToCall_ 它似乎也将我的变量向上转换为! 它似乎神奇地知道我给的属于并相应地投射。(void (Boo::*)(void))(void (Base::*)(void))instanceBoofuncToCallBoo

这是怎么做的?这是在运行时还是编译时完成的?它只是在我调用时尝试查找函数的名称(instance->*funcToCall)();吗?

不要吹毛求疵typedef。我知道有些东西需要typedef为了可读性。这只是测试代码。

编辑:我玩了更多的代码,它看起来很奇怪。我添加了一个新类,结果如下:

class Goo : public Base
{
public:
    void callingFunc()
    {
        cout << "Yo there" << endl;
    }
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();
    Goo * goo = new Goo();

    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // outputs "Hey there" not "Yo there"
}

在这一点上,它有点有意义,但也有点没有意义。我的意思是,很明显它会从 Boo 中调用“Hey There”,但为什么代码不会爆炸?看起来非常危险。

编辑 2:发现一些真正令人不安和令人不安的东西。我调整了代码,所以接受一个计数器,这样我就有一个变量来检查正在发生的事情。

#include <iostream>

using namespace std;

class Base
{
};

typedef void(Base::*handler)();
#define selector(_SELECTOR) static_cast<handler>(&_SELECTOR)

class Boo : public Base
{
public:
    Boo() : counter(0) {}
    void callingFunc()
    {
        cout << "Hey there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    int counter;

};

class Goo : public Base
{
public:
    Goo() : counter(0) {}
    void callingFunc()
    {
        cout << "Yo there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    int counter;
};

class Foo
{
public:
    void setCallback( Base * instance,  void (Base::*funcToCall)(void) )
    {
        this->instance = instance;
        this->funcToCall = funcToCall;
    }

    void doCall()
    {
        (instance->*funcToCall)();
    }

private:
    Base* instance;
    void (Base::*funcToCall)(void);
};

void main()
{
    Foo * foo = new Foo();
    Boo * boo = new Boo();
    Base * base = new Base();
    Goo * goo = new Goo();

    // first run
    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 0 at 0044BC60"

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 0 at 0044BC00"

    //second run
    foo->setCallback(goo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 1 at 0044BC60"

    foo->setCallback(boo, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there 1 at 0044BC00"

    // attempt with base
    foo->setCallback(base, selector(Boo::callingFunc) );
    foo->doCall(); // "Hey there *rubbish number* at  at 0044BC30"

}

现在我很确定函数回调是一个运行时的东西(显然是因为它没有给出编译错误,但我不确定,因为通常情况并非如此)。如果它是运行时的东西,那么它是有道理的,因为它几乎像脚本语言一样工作(按名称查找变量,如果存在则更新它等)。

我仍然需要有人来确认这一点。它确实看起来既强大又危险。我真的有一段时间没有看到这样的东西了。我现在太忙了,无法尝试打开组装中的东西来破译到底发生了什么。另外,我不擅长阅读它^^;;

编辑 3谢谢大家,现在一切都说得通了。villekulla 的回答让我相信,因为我的 Boo 和 Goo 类的结构相同,所以它能够以相同的方式访问“计数器”变量(如果您了解类和结构的内存是如何分配的,这应该很明显)。所以我在 Goo 插入了一个 'char' 变量:

class Goo : public Base
{
public:
    Goo() : counter(0) {}
    void callingFunc()
    {
        cout << "Yo there " << counter << " at " << &counter << endl;
        counter += 1;
    }

    char hey;
    int counter;
};

调用:

foo->setCallback(goo, selector(Boo::callingFunc) );
foo->doCall();

两次会产生胡言乱语,因为它正在抓取计数器应该在的字符(确认如上所述的未定义行为)。没有编译错误,因为……嗯……就编译而言,代码没有什么大错。

再次感谢!

4

2 回答 2

3

这是未定义的行为。

考虑为 Foo 和 Goo 添加一个成员,并调整 callFunc 以使用该成员:

class Boo : public Base
{
public:
  Boo()
    : m("Boo")
  {}
  void callingFunc()
  {
     cout << "Hey there, I'am " << boo << endl;
  }
  const char* m;
};

class Goo : public Base
{
public:
  Goo()
    : m("Goo")
  {}
  void callingFunc()
  {
     cout << "Yo there, I'am " << goo << endl;
  }
  const char* m;
};

在这种情况下

foo->setCallback(boo, selector(Boo::callingFunc) );

你得到输出

嘿,我是布

在这种情况下

foo->setCallback(goo, selector(Boo::callingFunc) );

你得到输出

嘿,我是咕

你清楚地看到 Boo::callingFunc 得到了一些 Goo 的实例......

这是数以百万计的例子之一,C++ 如何帮助自己在脚下开枪......只做标准允许的事情:/

你的例子没有爆炸,因为 callFunc 和 Goo/Foo 是微不足道的。如果不幸的是它永远不会爆炸,它“只会”引入奇怪的错误(Foo::callingFunc 处理 Goo::callingFunc 的数据)。

由于您没有使用虚函数,因此在获取地址时,所有函数调用地址都在编译时解析(在foo->setCallback(boo, selector(Boo::callingFunc) 行中);

于 2013-09-30T07:54:10.613 回答
2

如果您考虑如何在大多数现代 C++ 编译器中实现方法调用,可能会变得更清楚。非虚拟实例方法实际上是一个静态函数,它以这个指针作为第一个参数。例如:

class A {
  void f();
}

相当于

class A {
  static void f( A* this );
}

因此,当您传递指向实例方法的指针时,请将其视为传递指向静态函数的指针,第一个参数为 funcToCall(Base* this)。问题是 funcToCall 的真正实现认为你的实例指针的数据尊重 Foo 类,如果你的类有非平凡的数据,这可能会导致很多麻烦。这解释了为什么您的代码有效 - 它根本不使用实例数据。

对于实验,您可以尝试以下代码:

Boo* boo = 0;
boo->funcToCall();

如果您没有捕获调试断言,它会起作用,因为您的方法不使用任何实例数据。

于 2013-09-30T08:43:24.467 回答