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?
źródło
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).Odpowiedzi:
Interfejs opisuje, co trzeba wiedzieć, aby wejść w interakcję w danej sytuacji w kodzie. Po przedłużeniu interfejs typu „całej hierarchii”, Twój interface „pole powierzchni” staje się ogromna, co sprawia, że rozumowanie o tym trudniej .
Na przykład „szturchanie sąsiednich pomarańczy” oznacza, że ja, jako osoba trzecia, nie mogę naśladować bycia pomarańczą! Prywatnie zadeklarowałeś pomarańczowy typ, a następnie użyj RTTI, aby kod zachowywał się wyjątkowo podczas interakcji z tym typem. Jeśli chcę „być pomarańczowy”, to musi być w swoim prywatnym ogrodzie.
Teraz każdy, kto łączy się z „orangi”, łączy się z całym swoim pomarańczowym typem, a pośrednio z całym prywatnym ogrodem, zamiast z określonym interfejsem.
Chociaż na pierwszy rzut oka wygląda to na świetny sposób na rozszerzenie ograniczonego interfejsu bez konieczności zmiany wszystkich klientów (dodawania
am_I_orange
), zamiast tego zwykle powoduje to ossyzację bazy kodu i uniemożliwia dalsze rozszerzenie. Specjalna oranizm staje się nieodłącznym elementem funkcjonowania systemu i uniemożliwia stworzenie „mandarynkowego” zamiennika pomarańczy, który jest zaimplementowany w inny sposób i być może usuwa zależność lub elegancko rozwiązuje jakiś inny problem.Oznacza to, że Twój interfejs musi być wystarczający, aby rozwiązać problem. Z tego punktu widzenia, dlaczego trzeba tylko szturchać pomarańcze, a jeśli tak, to dlaczego w interfejsie niedostępna była oryginalność? Jeśli potrzebujesz rozmytego zestawu tagów, które można dodawać ad hoc, możesz dodać to do swojego typu:
Zapewnia to podobne ogromne rozszerzenie interfejsu z wąsko określonego do szerokiego opartego na tagach. Z wyjątkiem tego, że zamiast robić to przez RTTI i szczegóły implementacji (aka, „Jak zaimplementujesz? W przypadku pomarańczowego typu? Ok, zdasz.”), Robi to z czymś, co łatwo emuluje za pomocą zupełnie innej implementacji.
Można to nawet rozszerzyć na metody dynamiczne, jeśli tego potrzebujesz. - Czy popierasz bycie Foo'd z argumentami Baz, Tom i Alice? Ok, wzywam cię. W dużym sensie jest to mniej inwazyjne niż rzutowanie dynamiczne, aby dowiedzieć się, że drugi obiekt jest typem, który znasz.
Teraz obiekty mandarynki mogą mieć pomarańczowy znacznik i grać razem z nimi, podczas gdy ich implementacja jest oddzielona.
Wciąż może prowadzić do ogromnego bałaganu, ale jest to przynajmniej bałagan wiadomości i danych, a nie hierarchie wdrożeniowe.
Abstrakcja to gra polegająca na oddzielaniu i ukrywaniu nieistotności. Dzięki temu kod łatwiej jest rozumować lokalnie. RTTI wierci dziurę prosto przez abstrakcję do szczegółów implementacji. Może to ułatwić rozwiązanie problemu, ale wiąże się to z bardzo łatwym kosztem związania się z jedną konkretną implementacją.
źródło
dynamic_cast
(RTTI), w którym to przypadku tagi są zbędne. Druga możliwość, klasa Boga, jest odrażająca. Podsumowując, ta odpowiedź zawiera wiele słów, które moim zdaniem brzmią dobrze dla programistów Java, ale rzeczywista treść jest bez znaczenia.Większość moralnych perswazji przeciwko tej czy innej funkcji ma swoje źródło w spostrzeżeniu, że istnieje wiele błędnych zastosowań tej cechy.
Tam, gdzie moraliści zawodzą, to zakładają, że WSZYSTKIE zwyczaje są błędne, podczas gdy w rzeczywistości cechy istnieją z jakiegoś powodu.
Mają coś, co zwykłem nazywać „kompleksem hydraulików”: uważają, że wszystkie krany działają nieprawidłowo, ponieważ wszystkie krany, które mają naprawić, są. W rzeczywistości większość kranów działa dobrze: po prostu nie wzywasz do nich hydraulika!
Szaloną rzeczą, która może się zdarzyć, jest sytuacja, gdy programiści, aby uniknąć korzystania z danej funkcji, piszą dużo standardowego kodu, a w rzeczywistości prywatnie ponownie wdrażają dokładnie tę funkcję. (Czy zdarzyło Ci się spotkać klasy, które nie używają RTTI ani wywołań wirtualnych, ale mają wartość umożliwiającą śledzenie, który to rzeczywisty typ pochodny? To nic więcej niż przemyślenie RTTI w przebraniu).
Istnieje ogólny sposób myślenia o polimorfizmu:
IF(selection) CALL(something) WITH(parameters)
. (Przepraszam, ale programowanie, pomijając abstrakcję, polega na tym)Zastosowanie polimorfizmu w czasie projektowania (koncepcje) w czasie kompilacji (na podstawie dedukcji szablonu), w czasie wykonywania (dziedziczenie i oparte na funkcjach wirtualnych) lub na danych (RTTI i przełączanie) zależy od tego, ile decyzji jest znanych na każdym z etapów produkcji i jak zmienne są w każdym kontekście.
Chodzi o to, że:
im więcej możesz przewidzieć, tym większa szansa na wyłapanie błędów i uniknięcie błędów wpływających na użytkownika końcowego.
Jeśli wszystko jest stałe (łącznie z danymi), możesz zrobić wszystko za pomocą metaprogramowania szablonu. Po kompilacji na zaktualizowanych stałych cały program sprowadza się do instrukcji return, która wypluwa wynik .
Jeśli istnieje wiele przypadków, które są znane w czasie kompilacji , ale nie znasz faktycznych danych, na których muszą działać, rozwiązaniem może być polimorfizm w czasie kompilacji (głównie CRTP lub podobny).
Jeśli wybór przypadków zależy od danych (nie znanych wartości czasu kompilacji), a przełączanie jest jednowymiarowe (to, co należy zrobić, można zredukować tylko do jednej wartości), wówczas wysyłanie oparte na funkcjach wirtualnych (lub ogólnie „tabele wskaźników funkcji ") jest potrzebne.
Jeśli przełączanie jest wielowymiarowe, ponieważ w języku C ++ nie ma natywnych wysyłek wielu uruchomień , musisz:
Jeśli nie tylko przełączanie, ale nawet działania nie są znane w czasie kompilacji, wymagane jest tworzenie skryptów i parsowanie : same dane muszą opisywać akcję, która ma zostać na nich wykonana.
Teraz, ponieważ każdy z przypadków, które wyliczyłem, można postrzegać jako szczególny przypadek tego, co następuje, możesz rozwiązać każdy problem, nadużywając rozwiązania najniższego, również w przypadku problemów, na które można sobie pozwolić z najwyższym.
Właśnie tego moralizacja stara się unikać. Ale to nie znaczy, że problemy żyjące w najniższych domenach nie istnieją!
Uderzanie RTTI tylko po to, aby go uderzyć, jest jak walenie w niego
goto
tylko po to, by go uderzyć. Rzeczy dla papug, nie dla programistów.źródło
Na małym przykładzie wygląda to całkiem nieźle, ale w prawdziwym życiu wkrótce otrzymasz długi zestaw typów, które mogą się wzajemnie szturchać, niektóre z nich być może tylko w jednym kierunku.
A co
dark_orange_node
, czyblack_and_orange_striped_node
, czydotted_node
? Czy może mieć kropki w różnych kolorach? A jeśli większość kropek jest pomarańczowa, czy można je wtedy szturchnąć?Za każdym razem, gdy będziesz musiał dodać nową regułę, będziesz musiał wrócić do wszystkich
poke_adjacent
funkcji i dodać więcej instrukcji if.Jak zwykle trudno jest tworzyć ogólne przykłady, podam ci to.
Ale gdybym miał zrobić ten konkretny przykład, dodałbym
poke()
członka do wszystkich klas i pozwolił niektórym z nich zignorować wywołanie call (void poke() {}
), jeśli nie są zainteresowani.Z pewnością byłoby to nawet tańsze niż porównanie
typeid
s.źródło
supportsBehaviour
iinvokeBehaviour
każda klasa może mieć listę zachowań. Jednym z zachowań byłoby Poke i mogłoby zostać dodane do listy obsługiwanych zachowań przez wszystkie klasy, które chcą być zaczepiane.Myślę, że źle zrozumiałeś takie argumenty.
Istnieje wiele miejsc kodowania C ++, w których RTTI nie jest używane. Gdzie przełączniki kompilatora są używane do wymuszonego wyłączania RTTI. Jeśli kodujesz w ramach takiego paradygmatu ... to prawie na pewno zostałeś już poinformowany o tym ograniczeniu.
Dlatego problem dotyczy bibliotek . Oznacza to, że jeśli piszesz bibliotekę zależną od RTTI, wtedy Twoja biblioteka nie może być używana przez użytkowników, którzy wyłączyli RTTI. Jeśli chcesz, aby Twoja biblioteka była używana przez te osoby, nie może używać RTTI, nawet jeśli z Twojej biblioteki korzystają również osoby, które mogą korzystać z RTTI. Co równie ważne, jeśli nie możesz korzystać z RTTI, musisz trochę bardziej rozejrzeć się po bibliotekach, ponieważ korzystanie z RTTI jest dla Ciebie przełomem.
Jest wielu rzeczy, których nie robisz w gorących pętlach. Nie przydzielasz pamięci. Nie przechodzisz przez powiązane listy. I tak dalej. RTTI z pewnością może być kolejną z tych rzeczy „nie rób tego tutaj”.
Rozważ jednak wszystkie przykłady RTTI. We wszystkich przypadkach masz jeden lub więcej obiektów nieokreślonego typu i chcesz wykonać na nich jakąś operację, która może nie być możliwa dla niektórych z nich.
To jest coś, nad czym musisz pracować na poziomie projektowania . Możesz pisać kontenery, które nie alokują pamięci, które pasują do paradygmatu „STL”. Możesz uniknąć połączonych struktur danych list lub ograniczyć ich użycie. Możesz zreorganizować tablice struktur w struktury tablic lub cokolwiek innego. To zmienia pewne rzeczy, ale możesz zachować to w podziale.
Zmieniasz złożoną operację RTTI w zwykłe wywołanie funkcji wirtualnej? To kwestia projektu. Jeśli musisz to zmienić, to jest to coś, co wymaga zmian w każdej klasie pochodnej. Zmienia to, jak dużo kodu współdziała z różnymi klasami. Zakres takiej zmiany wykracza daleko poza krytyczne dla wydajności sekcje kodu.
Więc ... dlaczego napisałeś to w niewłaściwy sposób?
W jakim celu?
Mówisz, że klasa bazowa jest „chuda i wredna”. Ale tak naprawdę ... to nie istnieje . W rzeczywistości nic nie robi .
Wystarczy spojrzeć na przykład:
node_base
. Co to jest? Wydaje się, że jest to rzecz, która sąsiaduje z innymi rzeczami. To jest interfejs Java (w tym przypadku Java pre-generics): klasa, która istnieje wyłącznie po to, by być czymś, co użytkownicy mogą rzutować na rzeczywisty typ. Może dodasz jakąś podstawową funkcję, taką jak sąsiedztwo (dodaje JavaToString
), ale to wszystko.Istnieje różnica między „chudym i wrednym” a „przejrzystym”.
Jak powiedział Yakk, takie style programowania ograniczają się pod względem interoperacyjności, ponieważ jeśli cała funkcjonalność znajduje się w klasie pochodnej, wówczas użytkownicy spoza tego systemu, bez dostępu do tej klasy pochodnej, nie mogą współdziałać z systemem. Nie mogą zastępować funkcji wirtualnych i dodawać nowych zachowań. Nie mogą nawet wywołać tych funkcji.
Ale to, co robią, sprawia, że robienie nowych rzeczy, nawet w ramach systemu, jest poważnym problemem. Rozważ swoją
poke_adjacent_oranges
funkcję. Co się stanie, jeśli ktoś będzie chciałlime_node
typu, który można szturchać tak jakorange_node
s? Cóż, nie możemy wywodzić sięlime_node
zorange_node
; to nie ma sensu.Zamiast tego musimy dodać nowy
lime_node
pochodzący znode_base
. Następnie zmień nazwępoke_adjacent_oranges
napoke_adjacent_pokables
. A potem spróbuj przesyłać doorange_node
ilime_node
; którakolwiek obsada jest tą, którą szturchamy.Jednak
lime_node
potrzebuje własnegopoke_adjacent_pokables
. Ta funkcja musi wykonywać te same kontrole rzutowania.A jeśli dodamy trzeci typ, musimy nie tylko dodać jego własną funkcję, ale musimy zmienić funkcje w pozostałych dwóch klasach.
Oczywiście teraz tworzysz
poke_adjacent_pokables
darmową funkcję, aby działała dla nich wszystkich. Ale jak myślisz, co się stanie, jeśli ktoś doda czwarty typ i zapomni dodać go do tej funkcji?Witam, cicha przerwa . Program wydaje się działać mniej więcej dobrze, ale tak nie jest. Gdyby
poke
była rzeczywistą funkcją wirtualną, kompilator zawiódłby, gdybyś nie nadpisał czystej funkcji wirtualnej znode_base
.Na swój sposób nie masz takich kontroli kompilatora. Oczywiście, kompilator nie sprawdzi, czy wirtualne nie są czyste, ale przynajmniej masz ochronę w przypadkach, w których ochrona jest możliwa (tj. Nie ma domyślnej operacji).
Stosowanie przezroczystych klas podstawowych z RTTI prowadzi do koszmaru konserwacji. Rzeczywiście, większość zastosowań RTTI prowadzi do bólu głowy związanego z utrzymaniem. Nie oznacza to, że RTTI nie jest przydatne (jest na przykład niezbędne do
boost::any
pracy). Ale jest to bardzo wyspecjalizowane narzędzie do bardzo specjalistycznych potrzeb.W ten sposób jest „szkodliwy” w taki sam sposób jak
goto
. To przydatne narzędzie, którego nie należy pozbywać się. Ale jego użycie powinno być rzadkie w twoim kodzie.Tak więc, jeśli nie możesz użyć przezroczystych klas bazowych i dynamicznego rzutowania, jak uniknąć grubych interfejsów? Jak uniknąć bulgotania każdej funkcji, którą możesz chcieć wywołać dla typu, z propagacji do klasy bazowej?
Odpowiedź zależy od tego, do czego służy klasa bazowa.
Przezroczyste klasy bazowe, takie jak
node_base
po prostu używają niewłaściwego narzędzia do rozwiązania problemu. Połączone listy najlepiej obsługiwać za pomocą szablonów. Typ węzła i sąsiedztwo będą dostarczane przez typ szablonu. Jeśli chcesz umieścić na liście typ polimorficzny, możesz. Po prostu użyjBaseClass*
jakT
w argumencie szablonu. Lub preferowany inteligentny wskaźnik.Ale są inne scenariusze. Jeden to typ, który robi wiele rzeczy, ale ma kilka opcjonalnych części. Konkretna instancja może implementować pewne funkcje, a inna nie. Jednak konstrukcja tego typu zwykle daje właściwą odpowiedź.
Klasa „entity” jest tego doskonałym przykładem. Ta klasa od dawna nęka twórców gier. Koncepcyjnie ma gigantyczny interfejs, żyjący na przecięciu prawie tuzina całkowicie odmiennych systemów. A różne byty mają różne właściwości. Niektóre elementy nie mają żadnej reprezentacji wizualnej, więc ich funkcje renderujące nic nie robią. Wszystko to jest ustalane w czasie wykonywania.
Nowoczesnym rozwiązaniem jest system komponentowy.
Entity
to po prostu pojemnik z zestawem komponentów, pomiędzy którymi znajduje się trochę kleju. Niektóre składniki są opcjonalne; byt, który nie ma wizualnej reprezentacji, nie ma komponentu „grafiki”. Podmiot bez AI nie ma komponentu „kontrolera”. I tak dalej.Jednostki w takim systemie są po prostu wskaźnikami do komponentów, a większość ich interfejsu jest udostępniana poprzez bezpośredni dostęp do komponentów.
Opracowanie takiego systemu składowego wymaga uznania na etapie projektowania, że pewne funkcje są koncepcyjnie zgrupowane tak, że wszystkie typy wdrażające je wszystkie zaimplementują. Pozwala to wyodrębnić klasę z przyszłej klasy bazowej i uczynić z niej osobny składnik.
Pomaga to również w przestrzeganiu zasady pojedynczej odpowiedzialności. Taka skomponowana klasa ma jedynie obowiązek bycia posiadaczem komponentów.
Matthew Walton:
OK, zbadajmy to.
Aby to miało sens, musielibyśmy mieć sytuację, w której pewna biblioteka L udostępnia kontener lub inny ustrukturyzowany magazyn danych. Użytkownik może dodawać dane do tego kontenera, iterować po jego zawartości itp. Jednak biblioteka tak naprawdę nic nie robi z tymi danymi; po prostu zarządza swoim istnieniem.
Ale nie tyle zarządza swoim istnieniem, co zniszczeniem . Powodem jest to, że jeśli oczekuje się, że będziesz używać RTTI do takich celów, to tworzysz klasy, których L nie zna. Oznacza to, że twój kod przydziela obiekt i przekazuje go L do zarządzania.
Istnieją przypadki, w których coś takiego jest legalnym projektem. Sygnalizacja zdarzeń / przekazywanie komunikatów, kolejki robocze zabezpieczone wątkami itp. Ogólny wzorzec jest następujący: ktoś wykonuje usługę między dwoma fragmentami kodu, który jest odpowiedni dla dowolnego typu, ale usługa nie musi być świadoma konkretnych typów, których to dotyczy .
W języku C ten wzór jest zapisywany w formie pisowni
void*
, a jego użycie wymaga dużej ostrożności, aby uniknąć złamania. W C ++ ten wzorzec jest zapisywanystd::experimental::any
(wkrótce zostanie przeliterowanystd::any
).Sposób, w jaki to powinno działać, polega na tym, że L zapewnia
node_base
klasę, która przyjmuje A,any
która reprezentuje twoje rzeczywiste dane. Gdy otrzymasz wiadomość, element roboczy kolejki wątków lub cokolwiek robisz, rzutujesz toany
na odpowiedni typ, który znają zarówno nadawca, jak i odbiorca.Więc zamiast wynikające
orange_node
znode_data
, po prostu trzymać sięorange
wnętrzenode_data
„sany
dziedzinie członkowskim. Użytkownik końcowy wyodrębnia go i używaany_cast
do konwersji na plikorange
. Jeśli obsada się nie powiedzie, to tak nie jestorange
.Jeśli w ogóle znasz implementację
any
, prawdopodobnie powiesz: „hej, chwileczkę:any
wewnętrznie używa RTTI,any_cast
pracy”. Na co odpowiadam: „… tak”.To jest punkt abstrakcji . Głęboko w szczegółach, ktoś używa RTTI. Ale na poziomie, na którym powinieneś działać, bezpośrednie RTTI nie jest czymś, co powinieneś robić.
Powinieneś używać typów, które zapewniają żądaną funkcjonalność. W końcu tak naprawdę nie chcesz RTTI. To, czego potrzebujesz, to struktura danych, która może przechowywać wartość danego typu, ukryć ją przed wszystkimi z wyjątkiem pożądanego miejsca docelowego, a następnie przekonwertować ją z powrotem na ten typ, z weryfikacją, że przechowywana wartość faktycznie jest tego typu.
To się nazywa
any
. To używa RTTI, ale przy użyciuany
jest o wiele lepsze niż przy użyciu RTTI bezpośrednio, ponieważ bardziej poprawnie pasuje pożądanych semantykę.źródło
Jeśli wywołujesz funkcję, z reguły nie obchodzi Cię, jakie dokładnie kroki ona podejmie, tylko, że jakiś cel wyższego poziomu zostanie osiągnięty w ramach pewnych ograniczeń (a sposób, w jaki funkcja to robi, to tak naprawdę problem).
Kiedy używasz RTTI, aby dokonać preselekcji specjalnych obiektów, które mogą wykonać określoną pracę, podczas gdy inne w tym samym zestawie nie mogą, łamiesz ten wygodny pogląd na świat. Nagle dzwoniący powinien wiedzieć, kto może co zrobić, zamiast po prostu powiedzieć swoim sługom, żeby się tym zajął. Niektórym to przeszkadza i podejrzewam, że jest to duża część powodu, dla którego RTTI jest uważane za trochę brudne.
Czy występuje problem z wydajnością? Może, ale nigdy tego nie doświadczyłem i może to być mądrość sprzed dwudziestu lat lub od ludzi, którzy szczerze wierzą, że użycie trzech instrukcji montażu zamiast dwóch jest nie do przyjęcia.
Jak więc sobie z tym poradzić ... W zależności od sytuacji sensowne może być posiadanie właściwości specyficznych dla węzła w osobnych obiektach (tj. Całe „pomarańczowe” API może być oddzielnym obiektem). Obiekt główny mógłby wówczas mieć funkcję wirtualną zwracającą „pomarańczowy” interfejs API, zwracając domyślnie wartość nullptr dla obiektów innych niż pomarańczowy.
Chociaż może to być przesada w zależności od sytuacji, pozwoliłoby to na zapytanie na poziomie głównym, czy określony węzeł obsługuje określony interfejs API, a jeśli tak, wykonanie funkcji specyficznych dla tego interfejsu API.
źródło
C ++ jest zbudowany na idei statycznego sprawdzania typów.
[1] RTTI, to znaczy
dynamic_cast
itype_id
jest dynamicznym sprawdzaniem typu.Więc zasadniczo pytasz, dlaczego statyczne sprawdzanie typów jest lepsze niż dynamiczne sprawdzanie typów. A prosta odpowiedź brzmi, czy statyczne sprawdzanie typów jest lepsze od dynamicznego sprawdzania typów, zależy . Na dużo. Ale C ++ jest jednym z języków programowania, które zostały zaprojektowane w oparciu o ideę statycznego sprawdzania typów. A to oznacza, że np. Proces rozwoju, w szczególności testowanie, jest zazwyczaj dostosowywany do statycznego sprawdzania typu, a następnie najlepiej do tego pasuje.
Re
możesz wykonać ten proces-heterogeniczne-węzły-wykresu ze statycznym sprawdzaniem typu i bez rzutowania za pomocą wzorca gości, np. w ten sposób:
Zakłada się, że znany jest zestaw typów węzłów.
Jeśli tak nie jest, wzorzec odwiedzających (istnieje wiele jego odmian) można wyrazić za pomocą kilku scentralizowanych rzutów lub tylko jednej.
Aby zapoznać się z podejściem opartym na szablonach, zobacz bibliotekę Boost Graph. Smutno powiedzieć, że go nie znam, nie korzystałem z niego. Więc nie jestem pewien, co dokładnie robi i jak, i w jakim stopniu używa statycznego sprawdzania typu zamiast RTTI, ale ponieważ Boost jest generalnie oparty na szablonach, a statyczne sprawdzanie typów jest główną ideą, myślę, że przekonasz się, że jego biblioteka Graph jest również oparta na statycznym sprawdzaniu typu.
[1] Informacje o typie czasu wykonywania .
źródło
Oczywiście jest scenariusz, w którym polimorfizm nie może pomóc: nazwy.
typeid
umożliwia dostęp do nazwy typu, chociaż sposób kodowania tej nazwy jest zdefiniowany w implementacji. Ale zwykle nie stanowi to problemu, ponieważ można porównać dwatypeid
-s:To samo dotyczy haszy.
szkodliwy jest zdecydowanie przesadzone: RTTI ma pewne wady, ale ma też zalety.
Naprawdę nie musisz używać RTTI. RTTI to narzędzie do rozwiązywania problemów OOP: jeśli użyjesz innego paradygmatu, prawdopodobnie znikną. C nie ma RTTI, ale nadal działa. Zamiast tego C ++ w pełni obsługuje OOP i zapewnia wiele narzędzi do rozwiązania problemu, który może wymagać informacji o czasie wykonywania: jednym z nich jest rzeczywiście RTTI, który ma swoją cenę. Jeśli nie możesz sobie na to pozwolić, rzecz, którą lepiej określić dopiero po bezpiecznej analizie wydajności, nadal istnieje stara szkoła
void*
: to nic nie kosztuje. Kosztuje mniej. Ale nie masz żadnego bezpieczeństwa typu. Więc chodzi o transakcje.Jeśli piszesz kod (prawdopodobnie ściśle) zgodny z C ++, możesz spodziewać się takiego samego zachowania niezależnie od implementacji. Implementacje zgodne ze standardami powinny obsługiwać standardowe funkcje języka C ++.
Ale weź pod uwagę, że w niektórych środowiskach C ++ definiuje („wolnostojące”), RTTI nie musi być dostarczane, nie ma też wyjątków
virtual
i tak dalej. RTTI potrzebuje warstwy bazowej do prawidłowego działania, która zajmuje się szczegółami niskiego poziomu, takimi jak ABI i informacje o rzeczywistym typie.Zgadzam się z Yakkiem co do RTTI w tym przypadku. Tak, można go użyć; ale czy jest to logicznie poprawne? Fakt, że język pozwala ominąć tę kontrolę, nie oznacza, że należy to zrobić.
źródło