Prawie każdy zasób C ++, który widziałem, który omawia tego rodzaju rzeczy, mówi mi, że powinienem preferować podejście polimorficzne od używania RTTI (identyfikacja typu w czasie wykonywania). Ogólnie rzecz biorąc, traktuję tego rodzaju rady poważnie i spróbuję zrozumieć uzasadnienie - w końcu C ++ to potężna bestia i trudna do zrozumienia w całej jej głębi. Jednak w przypadku tego konkretnego pytania rysuję puste miejsce i chciałbym zobaczyć, jakie porady może zaoferować internet. Najpierw podsumuję to, czego się do tej pory nauczyłem, wymieniając częste powody, dla których RTTI jest „uważane za szkodliwe”:
Niektóre kompilatory tego nie używają / RTTI nie zawsze jest włączone
Naprawdę nie kupuję tego argumentu. To tak, jakby powiedzieć, że nie powinienem używać funkcji C ++ 14, ponieważ istnieją kompilatory, które go nie obsługują. A jednak nikt nie zniechęciłby mnie do korzystania z funkcji C ++ 14. Większość projektów będzie miała wpływ na kompilator, którego używają, i sposób jego konfiguracji. Nawet cytując stronę podręcznika gcc:
-fno-rtti
Wyłącz generowanie informacji o każdej klasie z funkcjami wirtualnymi do użycia przez funkcje identyfikacji typu w czasie wykonywania C ++ (dynamic_cast i typeid). Jeśli nie używasz tych części języka, możesz zaoszczędzić trochę miejsca, używając tej flagi. Zauważ, że obsługa wyjątków używa tych samych informacji, ale G ++ generuje je w razie potrzeby. Operator dynamic_cast może być nadal używany do rzutów, które nie wymagają informacji o typie w czasie wykonywania, tj. Rzutowania do „void *” lub do jednoznacznych klas bazowych.
To mówi mi, że jeśli nie używam RTTI, mogę go wyłączyć. To tak, jakby powiedzieć, że jeśli nie używasz Boost, nie musisz się z nim łączyć. Nie muszę planować przypadku, w którym ktoś się kompiluje -fno-rtti
. Ponadto w tym przypadku kompilator zawiedzie głośno i wyraźnie.
To kosztuje dodatkową pamięć / może działać wolno
Ilekroć mam ochotę użyć RTTI, oznacza to, że muszę uzyskać dostęp do informacji o typie lub cechach mojej klasy. Jeśli zaimplementuję rozwiązanie, które nie korzysta z RTTI, zwykle oznacza to, że będę musiał dodać kilka pól do moich klas, aby przechowywać te informacje, więc argument pamięci jest trochę pusty (podam przykład tego poniżej).
Rzeczywiście, dynamic_cast może być powolny. Zwykle jednak istnieją sposoby, aby uniknąć konieczności używania go w sytuacjach krytycznych dla prędkości. I nie do końca widzę alternatywę. To TAK odpowiedź sugeruje użycie wyliczenia zdefiniowanego w klasie bazowej do przechowywania typu. Działa to tylko wtedy, gdy znasz wszystkie klasy pochodne a-priori. To dość duże „jeśli”!
Z tej odpowiedzi wynika również, że koszt RTTI również nie jest jasny. Różni ludzie mierzą różne rzeczy.
Eleganckie, polimorficzne projekty sprawią, że RTTI będzie niepotrzebne
Tego rodzaju rady traktuję poważnie. W tym przypadku po prostu nie mogę wymyślić dobrych rozwiązań innych niż RTTI, które obejmowałyby mój przypadek użycia RTTI. Podam przykład:
Powiedzmy, że piszę bibliotekę do obsługi wykresów jakiegoś rodzaju obiektów. Chcę zezwolić użytkownikom na generowanie własnych typów podczas korzystania z mojej biblioteki (więc metoda wyliczeniowa nie jest dostępna). Mam klasę bazową dla mojego węzła:
class node_base
{
public:
node_base();
virtual ~node_base();
std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};
Teraz moje węzły mogą być różnych typów. Co powiesz na te:
class red_node : virtual public node_base
{
public:
red_node();
virtual ~red_node();
void get_redness();
};
class yellow_node : virtual public node_base
{
public:
yellow_node();
virtual ~yellow_node();
void set_yellowness(int);
};
Do diabła, czemu nie jeden z tych:
class orange_node : public red_node, public yellow_node
{
public:
orange_node();
virtual ~orange_node();
void poke();
void poke_adjacent_oranges();
};
Ostatnia funkcja jest interesująca. Oto sposób na zapisanie tego:
void orange_node::poke_adjacent_oranges()
{
auto adj_nodes = get_adjacent_nodes();
foreach(auto node, adj_nodes) {
// In this case, typeid() and static_cast might be faster
std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
if (o_node) {
o_node->poke();
}
}
}
To wszystko wydaje się jasne i czyste. Nie muszę definiować atrybutów ani metod, w których ich nie potrzebuję, podstawowa klasa węzłów może pozostać szczupła i wredna. Bez RTTI, gdzie mam zacząć? Może mogę dodać atrybut node_type do klasy bazowej:
class node_base
{
public:
node_base();
virtual ~node_base();
std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
private:
std::string my_type;
};
Czy std :: string to dobry pomysł na typ? Może nie, ale z czego jeszcze mogę skorzystać? Wymyśl numer i masz nadzieję, że nikt inny go jeszcze nie używa? Co więcej, w przypadku mojego orange_node, co jeśli chcę użyć metod z red_node i yellow_node? Czy musiałbym przechowywać wiele typów na węzeł? Wydaje się to skomplikowane.
Wniosek
Te przykłady nie wydają się zbyt skomplikowane ani niezwykłe (pracuję nad czymś podobnym w mojej codziennej pracy, w której węzły reprezentują rzeczywisty sprzęt, który jest kontrolowany przez oprogramowanie i które robią bardzo różne rzeczy w zależności od tego, czym są). Jednak nie znałbym czystego sposobu na zrobienie tego za pomocą szablonów lub innych metod. Zwróć uwagę, że staram się zrozumieć problem, a nie bronić swojego przykładu. Moje czytanie stron, takich jak odpowiedź SO, do której linkowałem powyżej i ta strona na Wikibooks wydaje się sugerować, że nadużywam RTTI, ale chciałbym się dowiedzieć, dlaczego.
Wracając do mojego pierwotnego pytania: dlaczego „czysty polimorfizm” jest lepszy od stosowania RTTI?
node_base
jest częścią biblioteki, a użytkownicy będą tworzyć własne typy węzłów. Wtedy nie mogą zmodyfikować, node_base
aby zezwolić na inne rozwiązanie, więc może wtedy RTTI stanie się ich najlepszą opcją. Z drugiej strony istnieją inne sposoby zaprojektowania takiej biblioteki, tak aby nowe typy węzłów mogły być w niej znacznie bardziej eleganckie, bez konieczności używania RTTI (i innych sposobów projektowania nowych typów węzłów).