【问题标题】:C++: call (derived's) member function on base pointer of a different derived class's objectC ++:在不同派生类对象的基指针上调用(派生的)成员函数
【发布时间】:2013-04-18 17:03:30
【问题描述】:

在基类的指针上调用成员函数(指针)是否“安全”(和/或可移植),但指向的对象是不同派生类的实例。成员函数不访问派生类的任何成员变量或函数。

/* Shortened example of what happens in the client code and library */
class Base { /* ... */ }
class DerivedA : public Base {
    /* ... */ 
    public: void doSomethingA(float dt);
}
void DerivedA::doSomethingA(float dt) {
    /* Does not access members. Conventionally calls/accesses statics */
    cout << "dt(" << dt << ")";
}

class DerivedB : public Base { /* ... */ }

typedef void (Base::*SEL_SCHEDULE)(float);
SEL_SCHEDULE pCallback = (SEL_SCHEDULE)(&DerivedA::doSomethingA);

DerivedB db = new DerivedB();
Base *b = &db;
/* pCallback and b are saved in a list elsewhere (a scheduler) which calls */
(b->*pCallback)(0.f);

这似乎工作(在 MSVC/调试模式下)在运行时没问题,但我想知道这是否是 Bad (TM) - 为什么? (我还没有用 Android 和 iOS 的编译器测试这段代码)。

如果需要,请提供更多细节:我正在构建一个基于 cocos2d-x 的项目。 Base 是 CCObject、DerivedA 和 DerivedB 是 CCLayer 的子类。

层次结构是DerivedA 和DerivedB CCLayer CCNode CCObject。它们是游戏场景,它们在相互排斥的时间可见/活跃。

DerivedA 有一个不同的静态函数来设置音乐的播放,它接收一个 CCNode 调用者对象作为参数,schedules 另一个 selector (doSomethingA) 到 开始播放并慢慢淡入使用类似的东西:

callerNode->schedule(schedule_selector(DerivedA::doSomethingA), 0.05f);

schedule_selector 是 C 风格的演员。 doSomethingA 不访问其任何成员变量或调用成员函数。它访问静态成员并调用其他静态函数,例如such as

CocosDenshion::SimpleAudioEngine::sharedEngine()->setBackgroundMusicVolume(sFadeMusicVolume);

在运行时对doSomethingA 的调用发生在CCTimer::update。

hack 主要是为了避免重复代码并符合库的回调签名(定时器/调度器系统)。

【问题讨论】:

  • 您需要reinterpret_cast这一事实应该会敲响警钟。不幸的是,你隐藏了邪恶的 C 风格演员背后的危险。
  • 回调函数不使用任何类成员信息并简单地打印其参数这一事实隐藏了可能存在的错误。这当然不是正确的代码。回调的 C 风格类型转换破坏了类型安全。

标签: c++ cocos2d-x


【解决方案1】:

这是 UB。

你甚至可以使用 static_cast 来代替令人讨厌的 C 风格的演员表,而且演员表本身是非常合法的。但是

[注意:虽然 B 类不需要包含原始成员,但 成员指针所在对象的动态类型 取消引用必须包含原始成员;见 5.5。 —尾注] (5.2.9 12)

"第一个操作数称为对象表达式。如果动态 对象表达式的类型不包含 指针引用,行为未定义” (5.5 4)

即,当您从动态类型 DerivedB 的对象中调用它时,您会处于未定义状态。

现在,作为一个肮脏的黑客,它可能不是最糟糕的(比手动遍历 vtables 更好),但它真的需要吗?如果您不需要任何动态数据,为什么要在 DerivedB 上调用它? Base 在库中,您无法重新定义它。 Callback 也是图书管理员,所以你必须有这个typedef void (Base::*SEL_SCHEDULE)(float);,好的。但是为什么不能为 B 定义 doSomething 并指向它以与 DerivedB 的实例相结合呢?你说

doSomethingA 不访问其任何成员变量或调用 成员函数。它访问静态成员并调用其他静态成员 功能

但您也可以在doSomethingB 中进行操作。或者,如果您的回调与对象类型完全分离,并且您需要成员函数指针的唯一原因是符合库回调签名,您可以使您的实际回调非成员普通旧函数,并从一个调用它们-像DoSomething(float dt) {ReallyDoSomething(dt);}这样的线成员回调conformers。

【讨论】:

  • 由于在设置pCallback 时(在示例中;或在具体部分中调用callerNode-&gt;schedule),我们仍将传递指向DerivedA::doSomething 的指针,创建符合DerivedB::doSomething 重要吗?运行时的调用不会仍然是对 DerivedA::doSomething 的调用吗?也许我应该创建一个单独的 Derived 类(以符合库签名),它只实现 doSomething 并分别管理它的生命周期(DerivedA、DerivedB 和其他此类相互独占的类是它们的用法。)
  • 是的,你是对的,它仍然会与 DerivedA::doSomething 混淆得到 DerivedB 的 this。我没有注意到当你设置 pCallback 时你只有 DerivedA,而当你使用它时你只有 DerivedB。如果这意味着您将能够使用 Derived 动态类型的对象调用 Derived::DoSomething,那么使用单独的 Derived 的解决方案会更好。
【解决方案2】:

我想知道这是否是坏 (TM)

当然是这样,除非它是对在公共基类中声明的虚函数的覆盖。

如果是,那么您不需要狡猾的演员表;直接从&amp;Base::DoSomethingA初始化即可。

如果不是,那么邪恶的 C 类型转换(这里是伪装的 reinterpret_cast)允许您将指针应用到没有该成员函数的类型;称弗兰肯斯坦的可憎之物绝对可以做任何事情。如果函数实际上没有接触到对象,那么您很有可能不会看到任何不良影响;但你仍然坚持未定义的行为。

【讨论】:

    【解决方案3】:

    一般来说是不安全的。

    在这一行中,您使用 C 风格的回调函数破坏了类型安全系统:

    SEL_SCHEDULE pCallback = (SEL_SCHEDULE)(&DerivedA::doSomethingA);
    

    DerivedA 的成员函数应该只对 DerivedA 的实例(或进一步派生自它的东西)进行操作。你没有,你有一个 DerivedB,但是因为你的 C-cast,你的代码被编译了。

    如果您的回调函数实际上尝试访问 DerivedA 的成员,您可能会遇到严重的问题(未定义的行为)。

    因为它是只打印的函数,所以在这种情况下可能不是未定义的,但这并不意味着你应该这样做。

    一种类型安全的方法是使用带有 Base(引用)和 float 的回调,并使用 boost::bind 或 std::bind 来创建它。

    另一个简单的方法可能是你大部分时间的答案,就是调用 Base 的一个带浮点数的虚拟方法。

    【讨论】:

      猜你喜欢
      • 2017-07-18
      • 2016-03-17
      • 1970-01-01
      • 2011-08-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-23
      • 1970-01-01
      相关资源
      最近更新 更多