C ++:在不同派生类的对象的基指针上调用(派生的)成员函数

时间:2013-04-17 11:45:04

标签: c++ cocos2d-x

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

/* 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 / Debug模式下),但是我想知道这是不是Bad(TM) - 为什么? (我还没有用Android和iOS的编译器来测试这段代码。)

如果需要,可以提供更多细节:我正在构建基于cocos2d-x的项目。 BaseCCObjectDerivedADerivedBCCLayer的子类。

层次结构为DerivedADerivedB&lt; CCLayer&lt; CCNode&lt; CCObject。他们是游戏场景,它们在相互独立的时间可见/活着。

DerivedA有一个不同的静态函数来设置音乐播放,它接收CCNode来电者对象作为参数,schedules另一个selector schedule_selector }(doSomethingA开始播放并使用以下内容慢慢淡出

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

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

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

在运行时对doSomethingA的调用发生在{{3}}。

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

3 个答案:

答案 0 :(得分:1)

  

我想知道这是不是Bad(TM)

当然,除非它是在公共基类中声明的虚函数的重写。

如果是,那么你不需要狡猾的演员;只需直接从&Base::DoSomethingA初始化。

如果不是,那么邪恶的C演员(这里伪装成reinterpret_cast)允许你将指针应用于没有该成员函数的类型;称弗兰肯斯坦的憎恶可以做任何事情。如果该功能实际上没有碰到物体,那么很有可能你不会看到任何不良影响;但你仍然坚定不明的行为。

答案 1 :(得分:1)

一般来说不安全。

你已经破坏了类型安全系统,在这一行中使用回调的C风格转换:

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

DerivedA的成员函数应该仅对DerivedA的实例(或者从其进一步派生的东西)进行操作。你没有一个,你有一个DerivedB,但由于你的C-cast,你的代码被编译。

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

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

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

大多数时候可能是你的答案的另一个简单方法是调用一个浮动的Base的虚方法。

答案 2 :(得分:1)

这是UB。

你甚至可以使用static_cast代替可恶的C风格演员,而演员本身也是合法的。但是

  

[注意:虽然B类不需要包含原始成员,但是   指向成员的指针的动态类型   被解除引用必须包含原始成员;见5.5。 - 尾注](5.2.9 12)

     

“第一个操作数称为对象表达式。如果是动态的   对象表达式的类型不包含其所在的成员   指针指的是,行为未定义“(5.5 4)

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

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

  

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

但你也可以在doSomethingB中完成。或者,如果您的回调与对象类型完全解耦,并且您需要成员函数指针的唯一原因是与库回调签名的一致性,您可以使您的实际回调非成员普通旧函数,并从一个调用它们 - 线成员 - 回调构造者,如DoSomething(float dt) {ReallyDoSomething(dt);}