Posiadanie jednego obiektu głównego ogranicza to, co możesz zrobić i to, co kompilator może zrobić, bez większych korzyści.
Wspólna klasa główna umożliwia tworzenie kontenerów z czegokolwiek i wyodrębnianie ich za pomocą dynamic_cast
, ale jeśli potrzebujesz kontenerów z czymkolwiek, to coś podobnego boost::any
może zrobić bez wspólnej klasy root. A boost::any
także wspiera prymitywów - może nawet wspierać małe optymalizacji bufora i zostawić je prawie „rozpakowanych” w żargonie Java.
C ++ obsługuje i rozwija typy wartości. Zarówno literały, jak i typy wartości napisane przez programistę. Kontenery C ++ skutecznie przechowują, sortują, mieszają, konsumują i wytwarzają typy wartości.
Dziedziczenie, a zwłaszcza rodzaj monolitycznego dziedziczenia klas bazowych w stylu Java, wymaga typu „wskaźnikowego” lub „referencyjnego” opartego na wolnym magazynie. Twój uchwyt / wskaźnik / odniesienie do danych zawiera wskaźnik do interfejsu klasy i może polimorficznie reprezentować coś innego.
Chociaż jest to przydatne w niektórych sytuacjach, po ślubie ze wzorcem z „wspólną klasą bazową”, zablokowałeś całą bazę kodu na koszt i bagaż tego wzoru, nawet jeśli nie jest to przydatne.
Prawie zawsze wiesz więcej o typie niż „to obiekt” na stronie wywołującej lub w kodzie, który go używa.
Jeśli funkcja jest prosta, napisanie jej jako szablonu daje polimorfizm oparty na czasie kompilacji typu kaczego, w którym informacje na stronie wywołującej nie są wyrzucane. Jeśli funkcja jest bardziej złożona, można wykonać kasowanie typu, dzięki czemu można zbudować jednolite operacje na typie, który chcesz wykonać (powiedzmy, serializacja i deserializacja) i zapisać (w czasie kompilacji) do użycia (w czasie wykonywania) przez kod w innej jednostce tłumaczeniowej.
Załóżmy, że masz bibliotekę, w której chcesz, aby wszystko można było serializować. Jednym z podejść jest posiadanie klasy podstawowej:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
Teraz każdy fragment kodu, który piszesz, może być serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
Z wyjątkiem nie std::vector
, więc teraz musisz napisać każdy pojemnik. I nie te liczby całkowite, które otrzymałeś z biblioteki bignum. I nie tego typu, który napisałeś, że nie uważasz, że potrzebujesz serializacji. I nie a tuple
, int
ani a double
, ani a std::ptrdiff_t
.
Przyjmujemy inne podejście:
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 );
}
na co składa się, no cóż, pozornie nic nie robienie. Z wyjątkiem teraz możemy rozszerzyć write_to
, zastępując write_to
jako wolną funkcję w przestrzeni nazw typu lub metody w typie.
Możemy nawet napisać trochę kodu kasowania typu:
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) );
) {}
a teraz możemy wziąć dowolny typ i automatycznie umieścić go w ramce w can_serialize
interfejsie, który umożliwia serialize
późniejsze wywołanie za pośrednictwem interfejsu wirtualnego.
Więc:
void writer_thingy( can_serialize s );
jest funkcją, która bierze wszystko, co może serializować, zamiast
void writer_thingy( serialization_friendly const* s );
i pierwszy, w przeciwieństwie do drugiego, to poradzi sobie int
, std::vector<std::vector<Bob>>
automatycznie.
Nie trzeba było wiele pisać, szczególnie dlatego, że tego rodzaju rzeczy rzadko robisz, ale zyskaliśmy możliwość traktowania wszystkiego jako serializowalnego bez konieczności posiadania podstawowego typu.
Co więcej, możemy teraz nadać możliwość std::vector<T>
serializacji jako obywatel pierwszej klasy, po prostu nadpisując write_to( my_buffer*, std::vector<T> const& )
- z tym przeciążeniem można go przekazać do can_serialize
a serializowalność zapisów std::vector
jest przechowywana w vtable i dostępna przez .write_to
.
Krótko mówiąc, C ++ jest wystarczająco potężny, aby w razie potrzeby wdrożyć zalety jednej klasy bazowej w locie, bez konieczności płacenia ceny hierarchii wymuszonego dziedziczenia, gdy nie jest to wymagane. A czasy, w których wymagana jest pojedyncza baza (podrobiona lub nie), są dość rzadkie.
Gdy typy są w rzeczywistości ich tożsamością i wiesz, czym one są, możliwości optymalizacji są ogromne. Dane są przechowywane lokalnie i przylegle (co jest bardzo ważne dla przyjazności pamięci podręcznej na nowoczesnych procesorach), kompilatory mogą łatwo zrozumieć, co robi dana operacja (zamiast mieć nieprzejrzysty wskaźnik metody wirtualnej, który musi przeskakiwać, prowadząc do nieznanego kodu w po drugiej stronie), co pozwala optymalnie zmienić kolejność instrukcji, a mniej okrągłych kołków wbija się w okrągłe otwory.