C++ c++;设计:从基类强制转换为派生类,没有额外的数据成员
我写了很多处理消息协议的代码。消息协议通常会有一个通用消息帧,可以从串行端口或套接字反序列化;帧包含消息类型,必须根据消息类型处理消息有效负载 通常,我编写一组多态类,其中包含访问器方法和一个构造函数,该构造函数引用消息框架 但是,我突然想到,我可以直接从消息帧派生出访问器类,然后重新解释从消息帧转换到适当的访问器类,而不是基于对消息帧的引用构造访问器类。这使得代码更加简洁,并节省了一些字节和处理器周期 请参见下面的示例(非常做作和精简)。显然,对于生产代码,这一切都需要正确封装,强制转换成为派生类的成员,更好地分离关注点,并添加一些验证。为了提供一个简明的示例,我们删除了所有这些内容C++ c++;设计:从基类强制转换为派生类,没有额外的数据成员,c++,inheritance,C++,Inheritance,我写了很多处理消息协议的代码。消息协议通常会有一个通用消息帧,可以从串行端口或套接字反序列化;帧包含消息类型,必须根据消息类型处理消息有效负载 通常,我编写一组多态类,其中包含访问器方法和一个构造函数,该构造函数引用消息框架 但是,我突然想到,我可以直接从消息帧派生出访问器类,然后重新解释从消息帧转换到适当的访问器类,而不是基于对消息帧的引用构造访问器类。这使得代码更加简洁,并节省了一些字节和处理器周期 请参见下面的示例(非常做作和精简)。显然,对于生产代码,这一切都需要正确封装,强制转换成为派
#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;
}
#包括
#包括
#包括
结构通用消息
{
GenericMessage(const char*body):body(body,body+strlen(body)){}
std::向量体;
};
结构MessageType1:公共通用消息
{
int GetFoo()常量
{
返回体[2];
}
int GetBar()常量
{
返回体[3];
}
};
int main()
{
GenericMessage myGenericMessage(“1234”);
MessageType1*myMgessageType1=重新解释强制转换(&myGenericMessage);
std::cout以下是我不使用此技术的原因:
这违反了标准,并导致行为未定义。这可能是真的,几乎一直有效,但您不能排除将来会出现问题。编译器在优化中使用未定义的行为,这对毫无戒心的程序员来说是非常不利的。而且,您无法预测c在任何情况下,这都会发生
您不能保证您或您的团队成员都不会向派生类型添加某些数据成员。随着时间的推移,您的类层次结构将不断增长,并且会添加更多的代码;在某些情况下,您或其他程序员可能不清楚向派生类型添加无辜的数据成员(即使是暂时的,也许是出于某种调试目的)也可能意味着灾难
有干净合法的替代方案,例如使用基于引用的包装:
#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;
}
#包括
结构元素
{ };
结构电子振打器
{
Elem&Elem_;
ElemRapper(Elem&Elem):Elem_um(Elem)
{ }
};
结构ElemWrapper1:ElemWrapper
{
使用ElemWrapper::ElemWrapper;
void foo()
{std::cout不,你不能
它可能在您的情况下工作,但不建议这样做,因为(快速解释)派生类可能有更多的成员或虚拟函数,而这些成员或函数在基类中不可用
最简单的解决方案是保留继承方案(这很好),但使用工厂来实例化正确的消息类型。示例:
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\u cast
请注意,您可以将工厂放在任何位置,例如在GenericMessage类本身中,即
GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = myGenericMessage.get_specialized_message();
或者,您也可以从基本消息构建专门的消息,但最后是一样的:
GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = new MessageType1( myGenericMessage );
在许多应用程序中,如果添加以下测试,这已经足够好了:
static_assert(
sizeof(MessageType1) == sizeof(GenericMessage),
"Cannot overlay MessageType1 upon GenericMessage." );
没有编译器优化会改变派生类型的基类型片的布局,因此这通常是足够安全的
另外,使用static\u cast
reinterpret\u cast
用于比这更反常的事情
…好的,是的,是的,如果以下所有项均为真,则此操作可能失败:
GenericMessage
末尾有填充
MessageType1
中的成员(稍后添加)
- 您可以通过代码路径发送覆盖的
MessageType1
,该路径在写入之前从填充区读取
因此,权宜之计与稳健性之间进行权衡,然后做你认为最好的事情。你不是第一个使用这种模式的人,而且这也不是禁忌,尽管其他答案中都提到了这一点——尽管它们肯定是正确的,因为它有特殊的危险。我是否遗漏了一点,或者为什么不直接使用静态施法
或动态施法ic_cast
?因为它是一个下行模式,需要隐式cast@JBL:错误:从“GenericMessage*”到“MessageType1*”的转换无效…毫不奇怪,因为它正在从基指针转换为派生指针。它甚至不是向下转换的,因为myGenericMessage
的类型是GenericMessage
而不是MessageType1
@SimonElliott如果std::vector
不是实现定义的标准布局类,那么可能会有UB。除此之外,从注释中可以看出,代码很难理解,而且看起来很脆弱,一旦您的任何消息类型成为非标准布局,您就会得到UB。为什么不创建一个工厂来创建特定的布局消息类型而不是GenericMessage
?看起来太危险了,我无法存储在构造函数中传递的引用。如果它是临时对象,它将在构造之后立即被销毁。@KonstantinOznobihin我不明白你的意思。当然临时对象将被销毁,但这是一个问题吗?我想是elem_代码>成员将由ElemWrapper
成员函数使用,如果它引用的是已经被破坏的对象,这不是一个好主意。@KonstantinOznobihin包装器有一个非(!)常量引用,如果不复制是可能的。我想如果忽略问题所说的事实,这个答案是可以的“通常,我编写一组多态类,其中包含访问器方法和一个构造函数,该构造函数引用消息fram