Наличие одного корневого объекта ограничивает то, что вы можете делать и что может делать компилятор, без особой отдачи.
Общий корневой класс позволяет создавать контейнеры чего угодно и извлекать из них то, что они есть dynamic_cast
, но если вам нужны контейнеры чего угодно, то что-то похожее boost::any
может сделать это без общего корневого класса. А boost::any
также поддерживает примитивы - он может даже поддерживать оптимизацию небольших буферов и оставлять их почти «без коробки» на языке Java.
C ++ поддерживает и процветает на типах значений. И литералы, и программисты пишут типы значений. Контейнеры C ++ эффективно хранят, сортируют, хэшируют, потребляют и генерируют типы значений.
Наследование, особенно тот тип монолитного наследования, который подразумевают базовые классы в стиле Java, требует наличия «указателя» или «справочных» типов в хранилище. Ваш дескриптор / указатель / ссылка на данные содержит указатель на интерфейс класса и полиморфно может представлять что-то еще.
Хотя это полезно в некоторых ситуациях, после того, как вы вышли замуж за шаблон с «общим базовым классом», вы привязали всю свою кодовую базу к стоимости и багажу этого шаблона, даже если он бесполезен.
Почти всегда вы знаете больше о типе, чем "это объект", либо на вызывающем сайте, либо в коде, который его использует.
Если функция проста, то написание функции в виде шаблона дает вам полиморфизм времени компиляции типа утки, при котором информация на вызывающем сайте не выбрасывается. Если функция более сложная, стирание типа может быть выполнено, в результате чего единообразные операции над типом, который вы хотите выполнить (скажем, сериализация и десериализация), могут быть построены и сохранены (во время компиляции) для использования (во время выполнения) код в другой единице перевода.
Предположим, у вас есть библиотека, где вы хотите, чтобы все было сериализуемо. Один из подходов - иметь базовый класс:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
Теперь каждый бит кода, который вы пишете, может быть serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
За исключением не std::vector
, так что теперь вам нужно написать каждый контейнер. И не те целые числа, которые вы получили из этой библиотеки bignum. И не тот тип, который вы написали, что вы не нуждались в сериализации. И не a tuple
, int
или a double
, или a, или a std::ptrdiff_t
.
Мы используем другой подход:
void write_to( my_buffer* b, int x ) {
b->write_integer(x);
}
template<class T,
class=std::enable_if_t< void_t<
std::declval<T const*>()->write_to( std::declval<my_buffer*>()
> >
>
void write_to( my_buffer* b, T const* x ) {
if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
write_to( b, t );
}
который состоит из, ну, ничего не делать, по-видимому. За исключением того, что теперь мы можем расширять write_to
, переопределяя write_to
как свободную функцию в пространстве имен типа или метода в типе.
Мы можем даже написать немного кода стирания типа:
namespace details {
struct can_serialize_pimpl {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~can_serialize_pimpl() {}
};
}
struct can_serialize {
void write_to( my_buffer* b ) const { pImpl->write_to(b); }
void read_from( my_buffer const* b ) { pImpl->read_from(b); }
std::unique_ptr<details::can_serialize_pimpl> pImpl;
template<class T> can_serialize(T&&);
};
namespace details {
template<class T>
struct can_serialize : can_serialize_pimpl {
std::decay_t<T>* t;
void write_to( my_buffer*b ) const final override {
serialize( b, std::forward<T>(*t) );
}
void read_from( my_buffer const* ) final override {
deserialize( b, std::forward<T>(*t) );
}
can_serialize(T&& in):t(&in) {}
};
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}
и теперь мы можем взять произвольный тип и автоматически поместить его в can_serialize
интерфейс, который позволит вам вызывать serialize
его позже через виртуальный интерфейс.
Так:
void writer_thingy( can_serialize s );
это функция, которая принимает все, что может сериализовать, вместо
void writer_thingy( serialization_friendly const* s );
и первое, в отличие от вторых, он может работать int
, std::vector<std::vector<Bob>>
автоматически.
Это не заняло много времени, особенно потому, что такого рода вещи вам нужны редко, но мы получили возможность обрабатывать все как сериализуемые, не требуя базового типа.
Более того, теперь мы можем сделать std::vector<T>
сериализуемость в качестве первоклассного гражданина простым переопределением write_to( my_buffer*, std::vector<T> const& )
- с этой перегрузкой он может быть передан в a, can_serialize
и сериализуемость std::vector
запросов сохраняется в виртуальной таблице и доступна для них .write_to
.
Короче говоря, C ++ является достаточно мощным, чтобы вы могли реализовать преимущества одного базового класса на лету, когда это необходимо, без необходимости расплачиваться за иерархию принудительного наследования, когда она не требуется. И времена, когда требуется одна база (поддельная или нет), достаточно редки.
Когда типы фактически являются их идентичностью, и вы знаете, что они есть, возможностей для оптимизации предостаточно. Данные хранятся локально и непрерывно (что очень важно для удобства кэширования на современных процессорах), компиляторы могут легко понять, что делает данная операция (вместо того, чтобы иметь непрозрачный указатель виртуального метода, который она должна перепрыгивать, приводя к неизвестному коду на с другой стороны), что позволяет оптимально переупорядочивать инструкции, и меньшее количество круглых колышков забивается в круглые отверстия.