c ++设计:从基类转换为派生类,没有额外的数据成员

时间:2013-12-11 11:44:13

标签: 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;
}

我从未在任何地方看到过这种情况。如果派生没有其他数据成员,那么从基础转换到以这种方式派生是否有任何缺点?

3 个答案:

答案 0 :(得分:4)

这就是为什么我不会使用这种技术:

  1. 违反了标准,导致行为未定义。这种情况可能几乎一直在起作用,但你不能在将来排除问题。编译器have been seen在优化中使用未定义的行为,这对于毫无戒心的程序员来说是非常不利的。你无法预测何时以及在什么情况下会发生这种情况。

  2. 您不能保证您和队友都不会将某些数据成员添加到派生类型。您的类层次结构将会增长,并且会随着时间添加更多代码;在某些时候,对于您或其他程序员而言,向派生类型添加无辜数据成员(即使是暂时的,可能是出于某些调试目的)也可能不会显而易见。

  3. 有一些清晰合法的替代方案,例如使用基于引用的包装器:

    #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;
    }
    

答案 1 :(得分:1)

不,你不能!

可能在你的情况下工作,但它是不可取的,因为(快速解释)派生类可能有更多的成员或虚函数,这些都不会从基础上获得。

最简单的解决方案是保留继承方案(这很好),但使用工厂来实例化正确的消息类型。示例:

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 );

答案 2 :(得分:1)

如果添加以下测试,它在许多应用程序中就足够了:

static_assert(
    sizeof(MessageType1) == sizeof(GenericMessage),
        "Cannot overlay MessageType1 upon GenericMessage." );

没有编译器优化会改变派生类型的基类型slice的布局,所以这通常是足够安全的。

另外,使用 static_castreinterpret_cast 用于比这更反常的事情。


...好吧,是的,是的,当所有以下条件都为真时,这可能会失败:

  • GenericMessage 在末尾有填充
  • MessageType1 happens to lay inside that padding 中的成员(后来添加)。
  • 您通过代码路径发送覆盖 MessageType1,该代码路径读取该填充区在写入之前。< /li>

所以权衡权宜与稳健,然后做你认为最好的事情。您不是第一个使用这种模式的人,这也不是禁忌,尽管这里的其他答案令人心痛——尽管他们肯定是正确的,因为它具有特殊的危害。