在基类的指针上调用成员函数(指针)是否“安全”(和/或可移植),但指向的对象是不同派生类的实例。成员函数不访问派生类的任何成员变量或函数。
/* 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的项目。 Base
为CCObject
,DerivedA
和DerivedB
是CCLayer
的子类。
层次结构为DerivedA
和DerivedB
&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 主要是为了避免重复代码并符合库的回调签名(定时器/调度程序系统)。
答案 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::bind
或std::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);}
。