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