c ++设计:从基类转换为派生类,没有额外的数据成员
Posted
技术标签:
【中文标题】c ++设计:从基类转换为派生类,没有额外的数据成员【英文标题】:c++ design: cast from base to derived class with no extra data members 【发布时间】:2013-12-11 11:44:12 【问题描述】:我写了很多处理消息协议的代码。消息协议通常会有一个通用的消息帧,可以从串行端口或套接字反序列化;帧包含消息类型,消息负载必须根据消息类型进行处理。
通常我会编写一组多态类,其中包含访问器方法和一个构造函数,该构造函数引用消息帧。
我想到,我可以直接从消息帧派生访问器类,然后从消息帧重新解释到适当的访问器类,而不是基于对消息帧的引用构造访问器类。这使代码更简洁,并节省了一些字节和处理器周期。
请参阅下面的(非常人为和浓缩的)示例。显然,对于生产代码,这一切都需要被正确封装,强制转换成为派生类的成员,更好地分离关注点,并添加一些验证。为了组合一个简洁的例子,这一切都被删除了。
#include <iostream>
#include <cstring>
#include <vector>
struct GenericMessage
GenericMessage(const char* body):body_(body, body+strlen(body))
std::vector<char> body_;
;
struct MessageType1:public GenericMessage
int GetFoo()const
return body_[2];
int GetBar()const
return body_[3];
;
int main()
GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = reinterpret_cast<MessageType1*>(&myGenericMessage);
std::cout << "Foo:" << myMgessageType1->GetFoo() << std::endl;
std::cout << "Bar:" << myMgessageType1->GetBar() << std::endl;
return 0;
我从来没有在任何地方看到过这种情况。鉴于派生没有额外的数据成员,以这种方式从基转换为派生有什么缺点吗?
【问题讨论】:
我错过了一点,或者为什么不直接使用static_cast
或dynamic_cast
?
因为是向下转型,需要隐式转型
@JBL: 错误:从“GenericMessage*”到“MessageType1*”的无效转换......不出所料,因为它正在从基指针转换为派生指针。
@user1781290:关于 static_cast 的要点。 dynamic_cast 不起作用,因为 typeid 不匹配。
@SimonElliott 如果std::vector<char>
不是实现定义的标准布局类,则可能存在UB。除此之外,正如您从 cmets 看到的那样,代码很难理解并且看起来很脆弱,只要您的任何消息类型变成非标准布局,您就会得到 UB。为什么不创建一个工厂来创建特定的消息类型而不是GenericMessage
?
【参考方案1】:
这就是我不使用这种技术的原因:
这违反了标准并导致行为未定义。这可能是真的,这几乎一直有效,但你不能排除将来会出现问题。编译器have been seen 在优化中利用未定义的行为,这对毫无戒心的程序员来说非常不利。而且你无法预测这会在什么时候以及在什么情况下发生。
您不能保证您和团队成员都不会将某些数据成员添加到派生类型。随着时间的推移,您的类层次结构将会增长,并且会添加更多代码;在某些时候,您或其他程序员可能并不明显将无辜的数据成员添加到派生类型(即使是暂时的,可能出于某种调试目的)可能会带来灾难。
有干净且合法的替代方案,例如使用基于引用的包装器:
#include <iostream>
struct Elem
;
struct ElemWrapper
Elem &elem_;
ElemWrapper(Elem &elem) : elem_(elem)
;
struct ElemWrapper1 : ElemWrapper
using ElemWrapper::ElemWrapper;
void foo()
std::cout << "foo1" << std::endl;
;
struct ElemWrapper2 : ElemWrapper
using ElemWrapper::ElemWrapper;
void foo()
std::cout << "foo2" << std::endl;
;
int main()
Elem e;
ElemWrapper1(e).foo();
return 0;
【讨论】:
对我来说存储在构造函数中传递的引用太危险了。如果它是临时对象,它将在构建后立即销毁。 @KonstantinOznobihin 我不明白你的意思。当然临时对象会被销毁,但是为什么会出现问题呢? 我认为elem_
成员是由ElemWrapper
成员函数使用的,如果它引用已经被破坏的对象,这不是一个好主意。
@KonstantinOznobihin 包装器有一个非 (!) const 引用,因此不能复制。
我想这个答案是可以的,如果你忽略了这样一个事实,即“通常我用访问器方法和一个引用消息框架的构造函数编写一组多态类。” - 这基本上是您建议的替代“解决方案”,当他没有要求解决方案时,而是“以这种方式从基础转换为派生的任何缺点,因为派生没有额外的数据成员”,这很容易由代码生成器(通常在这些场景中使用)或静态分析强制执行。【参考方案2】:
不,你不能!
它可能适用于您的情况,但不建议这样做,因为(快速解释)派生类可能有更多的成员或虚函数,而这些成员或虚函数在基类中是不可用的。
最简单的解决方案是保留您的继承方案(这很好),但使用工厂来实例化正确的消息类型。示例:
struct GenericMessage* create_message(const char* body)
int msg_type = body[5]; // I don't know where type is coded, this is an example
switch(msg_type)
case 1:
return new MessageType1(body);
break;
// etc.
您可以稍后安全地dynamic_cast
。
请注意,您可以将工厂放在任何地方,例如在 GenericMessage 类本身中,即
GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = myGenericMessage.get_specialized_message();
或者,您也可以从基础消息构建专门的消息,但最后是一样的:
GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = new MessageType1( myGenericMessage );
【讨论】:
【参考方案3】:如果您添加以下测试,它在许多应用程序中已经足够了:
static_assert(
sizeof(MessageType1) == sizeof(GenericMessage),
"Cannot overlay MessageType1 upon GenericMessage." );
没有编译器优化会改变派生类型的基类型slice的布局,所以这通常是足够安全的。
另外,请使用static_cast
。 reinterpret_cast
用于比这更反常的事情。
...好的,是的,是的,当以下所有都为真时,这可能会失败:
GenericMessage
在末尾有填充 。
MessageType1
happens to lay inside that padding 中的成员(后来添加)。
您覆盖 MessageType1
通过从该填充区域在写入之前读取的代码路径发送。
因此,权衡权宜性与稳健性,然后做您认为最好的事情。您不是第一个使用这种模式的人,这也不是禁忌,尽管这里的其他答案令人费解——尽管它们肯定是正确的,它具有特殊的危害。
【讨论】:
以上是关于c ++设计:从基类转换为派生类,没有额外的数据成员的主要内容,如果未能解决你的问题,请参考以下文章