Typy wewnętrzne w C ++ - dobry styl czy zły styl?

179

Coś, co ostatnio często robię, to deklarowanie typów definicji odpowiednich dla określonej klasy wewnątrz tej klasy, tj

class Lorem
{
    typedef boost::shared_ptr<Lorem> ptr;
    typedef std::vector<Lorem::ptr>  vector;

//
// ...
//
};

Te typy są następnie używane w innym miejscu w kodzie:

Lorem::vector lorems;
Lorem::ptr    lorem( new Lorem() );

lorems.push_back( lorem );

Powody, dla których to lubię:

  • Zmniejsza hałas wprowadzany przez szablony klas, std::vector<Lorem>staje się Lorem::vectoritp.
  • Służy jako deklaracja intencji - w powyższym przykładzie klasa Lorem ma być odniesieniem liczonym przez boost::shared_ptri przechowywanym w wektorze.
  • Pozwala to na zmianę implementacji - tj. Gdyby Lorem wymagał zmiany, aby inwazyjne zliczanie referencji (przez boost::intrusive_ptr) na późniejszym etapie, miałoby to minimalny wpływ na kod.
  • Myślę, że wygląda „ładniej” i jest prawdopodobnie łatwiejszy do odczytania.

Powody, dla których tego nie lubię:

  • Czasami występują problemy z zależnościami - jeśli chcesz osadzić, powiedzmy, a Lorem::vectorw innej klasie, ale potrzebujesz (lub chcesz) przekazać dalej deklarację Lorem (w przeciwieństwie do wprowadzania zależności od jej pliku nagłówkowego), w końcu będziesz musiał użyć jawne typy (np. boost::shared_ptr<Lorem>zamiast Lorem::ptr), co jest trochę niespójne.
  • Może nie jest to zbyt częste i przez to trudniejsze do zrozumienia?

Staram się być obiektywny w swoim stylu kodowania, więc dobrze byłoby poznać inne opinie na jego temat, abym mógł trochę przeanalizować swoje myślenie.

Will Baker
źródło

Odpowiedzi:

153

Uważam, że to doskonały styl i sam go używam. Zawsze najlepiej jest ograniczyć zakres nazw tak bardzo, jak to tylko możliwe, a użycie klas jest najlepszym sposobem na to w C ++. Na przykład biblioteka C ++ Standard w dużym stopniu wykorzystuje typedefs w klasach.


źródło
To dobra uwaga, zastanawiam się, że moja podświadomość wyglądała „ładniej”, delikatnie wskazując, że ograniczony zakres jest dobrą rzeczą. Zastanawiam się jednak, czy fakt, że STL używa go głównie w szablonach klas, sprawia, że ​​jest to nieco inne użycie? Czy trudniej jest usprawiedliwić się w „konkretnej” klasie?
Will Baker
1
Cóż, biblioteka standardowa składa się z szablonów, a nie klas, ale myślę, że uzasadnienie jest takie samo dla obu.
14

Służy jako deklaracja intencji - w powyższym przykładzie klasa Lorem ma być referencją liczoną przez boost :: shared_ptr i przechowywaną w wektorze.

Właśnie tego nie robi.

Jeśli widzę w kodzie „Foo :: Ptr”, nie mam absolutnie pojęcia, czy jest to shared_ptr czy Foo * (w STL są :: pointer typedefs, które są T *, pamiętaj), czy cokolwiek innego. Esp. jeśli jest to wskaźnik współdzielony, nie dostarczam w ogóle typedef, ale zachowuję użycie shared_ptr jawnie w kodzie.

Właściwie rzadko używam czcionek poza metaprogramowaniem szablonów.

STL robi tego typu rzeczy przez cały czas

Projekt STL z pojęciami zdefiniowanymi w kategoriach funkcji składowych i zagnieżdżonych typówef jest historyczną ślepą uliczką, nowoczesne biblioteki szablonów używają bezpłatnych funkcji i klas cech (por. Boost.Graph), ponieważ nie wykluczają one wbudowanych typów z modelowanie koncepcji i ponieważ ułatwia adaptację typów, które nie zostały zaprojektowane z myślą o koncepcjach określonych bibliotek szablonów.

Nie używaj STL jako powodu do popełnienia tych samych błędów.

Marc Mutz - mmutz
źródło
Zgadzam się z twoją pierwszą częścią, ale twoja ostatnia edycja jest trochę krótkowzroczna. Takie zagnieżdżone typy upraszczają definicję klas cech, ponieważ zapewniają rozsądną wartość domyślną. Rozważ nową std::allocator_traits<Alloc>klasę ... nie musisz jej specjalizować dla każdego pojedynczego alokatora, który piszesz, ponieważ po prostu pożycza typy bezpośrednio z Alloc.
Dennis Zickefoose
@Dennis: W C ++ wygoda powinna leżeć po stronie / użytkownika / biblioteki, a nie po stronie jej / autora /: użytkownik życzy sobie jednolitego interfejsu dla cechy i tylko klasa cechy może to zapewnić, z powodów podanych powyżej). Ale nawet jako Allocautorowi nie jest trudniej wyspecjalizować się std::allocator_traits<>w nowym typie, niż dodać potrzebne typy. Zredagowałem również odpowiedź, ponieważ moja pełna odpowiedź nie mieściła się w komentarzu.
Marc Mutz - mmutz,
Ale jest po stronie użytkownika. Jako użytkownik z allocator_traitspróbą utworzenia niestandardowego przydzielania, nie muszę męczyć się z piętnastu członków klasy cechami ... wszystko, co musisz zrobić, to powiedzieć typedef Blah value_type;i zapewnić odpowiednie funkcje składowe, a domyślnym allocator_traitsbędzie dowiedzieć się odpoczynek. Spójrzmy dalej na przykład Boost.Graph. Tak, intensywnie wykorzystuje klasę cech ... ale domyślna implementacja graph_traits<G>po prostu pyta Go własne wewnętrzne typy definicji.
Dennis Zickefoose
1
I nawet standardowa biblioteka 03 wykorzystuje klasy cech tam, gdzie jest to właściwe ... Filozofią biblioteki nie jest operowanie na kontenerach w sposób ogólny, ale operowanie na iteratorach. Zapewnia iterator_traitswięc klasę, dzięki której ogólne algorytmy mogą łatwo wyszukiwać odpowiednie informacje. Co znowu, domyślnie wysyła zapytanie do iteratora o jego własne informacje. Krótko mówiąc, cechy i typy wewnętrzne prawie się nie wykluczają ... wspierają się nawzajem.
Dennis Zickefoose
1
@Dennis: iterator_traitsstało się konieczne, ponieważ T*powinien być modelem RandomAccessIterator, ale nie możesz umieścić wymaganych czcionek w T*. Kiedy już to zrobiliśmy iterator_traits, zagnieżdżone typy stały się zbędne i żałuję, że nie zostały usunięte tam i wtedy. Z tego samego powodu (niemożność dodania wewnętrznych typów czcionek ) T[N]nie modeluje Sequencekoncepcji STL i potrzebujesz kludges, takich jak std::array<T,N>. Boost.Range pokazuje, jak można zdefiniować nowoczesną koncepcję Sequence, która T[N]może modelować, ponieważ nie wymaga zagnieżdżonych typów definicji ani funkcji składowych.
Marc Mutz - mmutz
8

Typdefy są zdecydowanie w dobrym stylu. Wszystkie twoje „powody, które lubię” są dobre i poprawne.

O problemach, które masz z tym. Cóż, deklaracja naprzód nie jest świętym Graalem. Możesz po prostu zaprojektować swój kod, aby uniknąć wielopoziomowych zależności.

Możesz przenieść typedef poza klasę, ale Class :: ptr jest o wiele ładniejszy niż ClassPtr, że nie robię tego. To jest jak z przestrzeniami nazw, jak dla mnie - wszystko pozostaje połączone w zakresie.

Czasami to robiłem

Trait<Loren>::ptr
Trait<Loren>::collection
Trait<Loren>::map

Może to być ustawienie domyślne dla wszystkich klas domen i z pewną specjalizacją dla niektórych.

Mykoła Golubyev
źródło
3

STL robi tego typu rzeczy przez cały czas - typedefs są częścią interfejsu wielu klas w STL.

reference
iterator
size_type
value_type
etc...

to wszystkie typy definicji, które są częścią interfejsu różnych klas szablonów STL.

Michael Burr
źródło
To prawda i podejrzewam, że właśnie tutaj go odebrałem. Wygląda na to, że byłoby to trochę łatwiejsze do uzasadnienia? Nie mogę się powstrzymać od spojrzenia na typedefs w szablonie klasy jako bardziej zbliżone do zmiennych, jeśli myślisz w kategoriach „metaprogramowania”.
Will Baker
3

Kolejny głos za dobrym pomysłem. Zacząłem to robić, pisząc symulację, która miała być wydajna zarówno w czasie, jak i przestrzeni. Wszystkie typy wartości miały definicję typu Ptr, która zaczynała się jako wspólny wskaźnik doładowania. Następnie wykonałem pewne profilowanie i zmieniłem niektóre z nich na natrętny wskaźnik doładowania bez konieczności zmiany kodu, w którym te obiekty były używane.

Zwróć uwagę, że działa to tylko wtedy, gdy wiesz, gdzie będą używane klasy i że wszystkie zastosowania mają takie same wymagania. Nie użyłbym tego na przykład w kodzie biblioteki, ponieważ pisząc bibliotekę nie możesz wiedzieć, w jakim kontekście będzie ona używana.

KeithB
źródło
3

Obecnie pracuję nad kodem, który intensywnie wykorzystuje tego typu typy. Jak dotąd to jest w porządku.

Ale zauważyłem, że dość często istnieją iteracyjne typy typów, definicje są podzielone na kilka klas i nigdy tak naprawdę nie wiesz, z jakim typem masz do czynienia. Moim zadaniem jest podsumowanie rozmiaru niektórych złożonych struktur danych ukrytych za tymi typami czcionek - więc nie mogę polegać na istniejących interfejsach. W połączeniu z trzema do sześciu poziomami zagnieżdżonych przestrzeni nazw i wtedy staje się to mylące.

Dlatego przed ich użyciem należy wziąć pod uwagę kilka punktów

  • Czy ktoś jeszcze potrzebuje tych typów plików? Czy klasa jest często używana przez inne klasy?
  • Czy skrócić użytkowanie czy ukryć zajęcia? (W przypadku ukrycia możesz również pomyśleć o interfejsach.)
  • Czy inne osoby pracują z kodem? Jak oni to robią? Czy pomyślą, że będzie łatwiej, czy też będą zdezorientowani?
Bodo
źródło
1

Polecam przenieść te typy definicji poza klasę. W ten sposób usuniesz bezpośrednią zależność od współdzielonych klas wskaźników i wektorów i możesz je uwzględnić tylko wtedy, gdy jest to potrzebne. Jeśli nie używasz tych typów w swojej implementacji klasy, uważam, że nie powinny one być wewnętrznymi typedefami.

Powody, dla których to lubisz, są nadal dopasowane, ponieważ są rozwiązywane przez aliasy typu poprzez typedef, a nie przez deklarowanie ich wewnątrz klasy.

Cătălin Pitiș
źródło
To polutowałoby anonimową przestrzeń nazw za pomocą typedef, prawda ?! Problem z typedef polega na tym, że ukrywa on rzeczywisty typ, co może powodować konflikty, gdy jest dołączane do / przez wiele modułów, które są trudne do znalezienia / naprawienia. Dobrą praktyką jest umieszczanie ich w przestrzeniach nazw lub wewnątrz klas.
Indy9000
3
Konflikty nazw i anonimowe polution w przestrzeni nazw mają niewiele wspólnego z utrzymywaniem nazwy typu wewnątrz klasy lub poza nią. Możesz mieć konflikt nazw ze swoją klasą, a nie z typedefs. Aby więc uniknąć zanieczyszczenia nazw, używaj przestrzeni nazw. Zadeklaruj swoją klasę i powiązane definicje typów w przestrzeni nazw.
Cătălin Pitiș
Innym argumentem przemawiającym za umieszczeniem typedef wewnątrz klasy jest użycie funkcji opartych na szablonach. Powiedzmy, że funkcja otrzymuje nieznany typ kontenera (wektor lub listę) zawierający nieznany typ ciągu (ciąg lub własny wariant zgodny z ciągiem). jedynym sposobem na ustalenie typu ładunku kontenera jest wpisanie elementu typedef „value_type”, który jest częścią definicji klasy kontenera.
Marius
1

Kiedy typedef jest używany tylko w samej klasie (tj. Jest zadeklarowany jako prywatny), myślę, że to dobry pomysł. Jednak z dokładnie tych powodów, które podasz, nie użyłbym tego, jeśli krój pisma ma być znany poza klasą. W takim przypadku polecam przenieść je poza zajęcia.

Stefan Rådström
źródło