Наявність одного кореневого об'єкта обмежує те, що можна робити і що може робити компілятор, без особливих окупностей.
Звичайний кореневий клас дає змогу створювати контейнери з чого-небудь і витягувати ті, що вони є dynamic_cast
, а якщо вам потрібні контейнери-що-небудь, то щось подібне boost::any
може зробити це без загального кореневого класу. А boost::any
також підтримує примітиви - він навіть може підтримувати оптимізацію невеликих буферів і залишати їх майже "без коробки" в Java 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
зберігаються у vtable і доступ до них .write_to
.
Коротше кажучи, C ++ є достатньо потужним, що ви можете реалізовувати переваги одного базового класу на ходу, коли це потрібно, без необхідності платити ціну ієрархії примусового спадкування, коли цього не потрібно. А часи, коли потрібна єдина база (підроблена чи ні), досить рідкісні.
Коли типи фактично є їх ідентичністю, і ви знаєте, що вони є, можливостей для оптимізації є багато. Дані зберігаються локально та безперервно (що дуже важливо для зручності кешу на сучасних процесорах), компілятори можуть легко зрозуміти, що робить дана операція (замість того, щоб мати непрозорий віртуальний вказівник методу, який він повинен перескочити, ведучи до невідомого коду на інша сторона), яка дає змогу впорядкування оптимально впорядкувати, і менше круглих кілочок забиваються у круглі отвори.