С++ design: отбрасывается из базового класса в производный класс без дополнительных элементов данных

Я пишу довольно много кода, который обрабатывает протоколы сообщений. Довольно часто протокол сообщений будет иметь общий кадр сообщения, который можно десериализовать из последовательного порта или сокета; кадр содержит тип сообщения, и полезная нагрузка сообщения должна обрабатываться на основе типа сообщения.

Обычно я пишу полиморфный набор классов с помощью методов доступа и конструктор, который ссылается на кадр сообщения.

Мне кажется, что вместо того, чтобы создавать класс accessor на основе ссылки на фрейм сообщений, я мог бы просто получить классы доступа непосредственно из фрейма сообщения, а затем reinterpret_cast из кадра сообщения в соответствующий класс доступа. Это делает код более кратким и экономит несколько байтов и процессорных циклов.

См. пример (чрезвычайно надуманный и сжатый) ниже. Очевидно, что для производственного кода это все должно быть правильно инкапсулировано, литье сделало членом производного класса, улучшило разделение наложенных проблем и добавило некоторую валидацию. Все это было удалено для составления краткого примера.

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

Я никогда этого не видел. Есть ли недостаток в отливке от базы до производной таким образом, учитывая, что у производного нет дополнительных элементов данных?

Ответ 1

Вот почему я не буду использовать эту технику:

  • Это нарушение Стандарта и вызывает поведение undefined. Вероятно, это правда, что это работает почти все время, но вы не можете исключать проблемы в будущем. Компиляторы были замечены, чтобы использовать поведение undefined в оптимизации, во многом в ущерб не подозревающему программисту. И вы не можете предсказать, когда и при каких обстоятельствах это произойдет.

  • Вы не можете гарантировать, что ни вы, ни помощник по команде никогда не добавите некоторые члены данных в производный тип. Ваша иерархия классов будет расти, и со временем будет добавлен еще один код; в какой-то момент вам или другому программисту может быть не очевидно, что добавление невиновного элемента данных к производному типу (даже временно, возможно, для некоторой цели отладки) может означать катастрофу.

  • Существуют чистые и законные альтернативы, например, обертки, основанные на ссылках:

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

Ответ 2

Нет, вы не можете!

Это может работать в вашем случае, но это не рекомендуется, поскольку (быстрое объяснение) производный класс может иметь больше членов или виртуальных функций, которые не будут доступны из базы.

Самое простое решение - сохранить схему наследования (что хорошо), но используйте factory для создания правильного типа сообщения. Пример:

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 позже.

Обратите внимание, что вы можете разместить свой factory в любом месте, например, в самом классе GenericMessage, т.е.

GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = myGenericMessage.get_specialized_message();

В качестве альтернативы вы также можете создать специализированное сообщение из базового, но оно же в конце:

GenericMessage myGenericMessage("1234");
MessageType1* myMgessageType1 = new MessageType1( myGenericMessage );