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