Dlaczego warto używać klas zagnieżdżonych w C ++?

188

Czy ktoś może wskazać mi jakieś fajne zasoby do zrozumienia i korzystania z zagnieżdżonych klas? Mam trochę materiałów, takich jak zasady programowania i podobne rzeczy Centrum wiedzy IBM - Zagnieżdżone klasy

Ale wciąż mam problem ze zrozumieniem ich celu. Czy ktoś mógłby mi pomóc?

w okularach
źródło
15
Moja rada dla klas zagnieżdżonych w C ++ to po prostu nie używać klas zagnieżdżonych.
Billy ONeal
7
Są dokładnie jak zwykłe klasy ... z wyjątkiem zagnieżdżonych. Używaj ich, gdy wewnętrzna implementacja klasy jest tak złożona, że ​​najłatwiej można ją modelować za pomocą kilku mniejszych klas.
meagar
12
@Billy: Dlaczego? Wydaje mi się to zbyt szerokie.
John Dibling,
30
Nadal nie widziałem argumentu, dlaczego zagnieżdżone klasy są z natury złe.
John Dibling,
7
@ 7vies: 1. ponieważ to po prostu nie jest konieczne - możesz zrobić to samo z zewnętrznie zdefiniowanymi klasami, co zmniejsza zakres dowolnej zmiennej, co jest dobrą rzeczą. 2. ponieważ możesz zrobić wszystko, co może zrobić zagnieżdżona klasa typedef. 3. ponieważ dodają dodatkowy poziom wcięcia w środowisku, w którym unikanie długich linii jest już trudne 4. ponieważ deklarujesz dwa koncepcyjnie oddzielne obiekty w jednej classdeklaracji itp.
Billy ONeal

Odpowiedzi:

229

Zagnieżdżone klasy świetnie nadają się do ukrywania szczegółów implementacji.

Lista:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Tutaj nie chcę ujawniać Węzła, ponieważ inne osoby mogą zdecydować się na użycie klasy, co utrudniłoby mi aktualizację mojej klasy, ponieważ wszystko, co jest widoczne, jest częścią publicznego interfejsu API i musi zostać utrzymane zawsze . Ustawiając klasę jako prywatną, nie tylko ukrywam implementację, ale także mówię, że jest moja i mogę ją zmienić w dowolnym momencie, abyście nie mogli jej użyć.

Spójrz, std::listczy std::mapwszystkie zawierają ukryte klasy (czy też?). Chodzi o to, że mogą, ale nie muszą, ale ponieważ implementacja jest prywatna i ukryta, twórcy STL byli w stanie zaktualizować kod bez wpływu na sposób jego użycia lub pozostawienia dużej ilości starego bagażu leżącego wokół STL, ponieważ potrzebują aby zachować kompatybilność wsteczną z jakimś głupcem, który zdecydował, że chce użyć ukrytej w nim klasy Node list.

Martin York
źródło
9
Jeśli to robisz, Nodenie powinno być w ogóle widoczne w pliku nagłówkowym.
Billy ONeal
6
@Billy ONeal: Co zrobić, jeśli wykonuję implementację pliku nagłówkowego, takiego jak STL lub boost.
Martin York
6
@Billy ONeal: Nie. To kwestia dobrego projektu, a nie opinii. Umieszczenie go w przestrzeni nazw nie chroni przed użyciem. Jest to teraz część publicznego interfejsu API, który należy zachować na zawsze.
Martin York
21
@Billy ONeal: Chroni to przed przypadkowym użyciem. Dokumentuje również fakt, że jest prywatny i nie należy go używać (nie można go używać, chyba że zrobisz coś głupiego). Dlatego nie musisz go wspierać. Umieszczenie go w przestrzeni nazw powoduje, że jest on częścią publicznego interfejsu API (czegoś brakuje w tej rozmowie. Publiczny interfejs API oznacza, że ​​musisz go obsługiwać).
Martin York
10
@Billy ONeal: Zagnieżdżona klasa ma pewną przewagę nad zagnieżdżoną przestrzenią nazw: Nie można tworzyć instancji przestrzeni nazw, ale można tworzyć instancje klasy. Jeśli chodzi o detailkonwencję: Zamiast tego w zależności od takich konwencji należy pamiętać o sobie, lepiej polegać na kompilatorze, który śledzi je za Ciebie.
SasQ
142

Klasy zagnieżdżone są jak zwykłe klasy, ale:

  • mają dodatkowe ograniczenia dostępu (jak wszystkie definicje w definicji klasy),
  • oni nie zanieczyszczają daną przestrzeń nazw , np globalnej przestrzeni nazw. Jeśli uważasz, że klasa B jest tak głęboko związana z klasą A, ale obiekty A i B niekoniecznie są ze sobą powiązane, możesz chcieć, aby klasa B była dostępna tylko poprzez określenie zakresu klasy A (byłoby to określane jako A ::Klasa).

Kilka przykładów:

Publicznie zagnieżdżaj klasę, aby umieścić ją w zakresie odpowiedniej klasy


Załóżmy, że chcesz mieć klasę, SomeSpecificCollectionktóra agregowałaby obiekty klasy Element. Następnie możesz:

  1. zadeklaruj dwie klasy: SomeSpecificCollectioni Element- złe, ponieważ nazwa „Element” jest wystarczająco ogólna, aby spowodować ewentualne zderzenie nazwy

  2. wprowadzić przestrzeń nazw someSpecificCollectioni zadeklarować klasy someSpecificCollection::Collectioni someSpecificCollection::Element. Nie ma ryzyka kolizji nazwisk, ale czy może stać się bardziej gadatliwy?

  3. deklarują dwie globalne klasy SomeSpecificCollectioni SomeSpecificCollectionElement- co ma drobne wady, ale prawdopodobnie jest OK.

  4. deklaruj klasę globalną SomeSpecificCollectioni klasę Elementjako klasę zagnieżdżoną. Następnie:

    • nie ryzykujesz żadnych konfliktów nazw, ponieważ Element nie znajduje się w globalnej przestrzeni nazw,
    • w realizacji SomeSpecificCollectionodwołujesz się do sprawiedliwego Elementi wszędzie innego jako SomeSpecificCollection::Element- który wygląda + - tak samo jak 3., ale bardziej wyraźnie
    • staje się po prostu proste, że jest to „element określonej kolekcji”, a nie „konkretny element kolekcji”
    • widać, że SomeSpecificCollectionto także klasa.

Moim zdaniem ostatni wariant jest zdecydowanie najbardziej intuicyjny, a zatem najlepszy.

Pozwólcie, że podkreślę - nie jest to duża różnica w tworzeniu dwóch globalnych klas o bardziej pełnych nazwach. To tylko drobny szczegół, ale imho sprawia, że ​​kod jest bardziej przejrzysty.

Wprowadzenie innego zakresu w zakresie klasy


Jest to szczególnie przydatne do wprowadzania typedefs lub enum. Po prostu opublikuję tutaj przykład kodu:

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

Jeden wtedy zadzwoni:

Product p(Product::FANCY, Product::BOX);

Ale patrząc na propozycje uzupełnienia kodu Product::, często pojawia się lista wszystkich możliwych wartości wyliczenia (BOX, FANCY, CRATE) i łatwo jest tutaj popełnić błąd (wyliczone w C ++ 0x wyliczenia tego rodzaju rozwiązują to, ale nieważne ).

Ale jeśli wprowadzisz dodatkowy zakres dla tych wyliczeń za pomocą klas zagnieżdżonych, rzeczy mogą wyglądać następująco:

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Następnie połączenie wygląda następująco:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Następnie pisząc Product::ProductType:: IDE, otrzymasz tylko wyliczenia z sugerowanego pożądanego zakresu. Zmniejsza to również ryzyko popełnienia błędu.

Oczywiście może to nie być potrzebne w przypadku małych klas, ale jeśli ktoś ma dużo wyliczeń, ułatwia to programistom klienckim.

W ten sam sposób możesz „zorganizować” dużą grupę pism maszynowych w szablonie, jeśli kiedykolwiek zajdzie taka potrzeba. Czasami jest to użyteczny wzór.

Idiom PIMPL


PIMPL (skrót od Pointer to IMPLementation) to idiom przydatny do usuwania szczegółów implementacji klasy z nagłówka. Zmniejsza to potrzebę ponownej kompilacji klas w zależności od nagłówka klasy, ilekroć zmienia się część „implementacyjna” nagłówka.

Zwykle jest implementowany za pomocą zagnieżdżonej klasy:

Xh:

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp:

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Jest to szczególnie przydatne, jeśli pełna definicja klasy wymaga definicji typów z biblioteki zewnętrznej, która ma ciężki lub po prostu brzydki plik nagłówka (weź WinAPI). Jeśli używasz PIMPL, możesz zawrzeć dowolną funkcjonalność specyficzną dla WinAPI tylko w .cppi nigdy go nie włączać .h.

Kos
źródło
3
struct Impl; std::auto_ptr<Impl> impl; Ten błąd spopularyzował Herb Sutter. Nie używaj auto_ptr na niekompletnych typach, lub przynajmniej podejmij środki ostrożności, aby uniknąć generowania błędnego kodu.
Gene Bushuyev
2
@Billy ONeal: O ile mi wiadomo, możesz zadeklarować auto_ptrniepełny typ w większości implementacji, ale technicznie jest to UB w przeciwieństwie do niektórych szablonów w C ++ 0x (np. unique_ptr), Gdzie wyraźnie zaznaczono, że parametr szablonu może być niekompletny typ i gdzie dokładnie ten typ musi być kompletny. (np. użycie ~unique_ptr)
CB Bailey
2
@Billy ONeal: W C ++ 03 17.4.6.3 [lib.res.on.functions] mówi „W szczególności efekty są niezdefiniowane w następujących przypadkach: [...] jeśli jako argument szablonu użyty zostanie niekompletny typ podczas tworzenia wystąpienia komponentu szablonu ”. podczas gdy w C ++ 0x mówi „jeśli niekompletny typ jest użyty jako argument szablonu podczas tworzenia komponentu szablonu, chyba że jest to specjalnie dozwolone dla tego komponentu”. i później (na przykład): „Parametr szablon Tod unique_ptrmoże być niekompletna typu”.
CB Bailey,
1
@MilesRout To zbyt ogólne. Zależy od tego, czy kod klienta może dziedziczyć. Reguła: Jeśli masz pewność, że nie usuniesz wskaźnika klasy bazowej, wirtualny dtor jest całkowicie redundantny.
Kos,
2
@IsaacPascual aww, powinienem zaktualizować to teraz, gdy mamy enum class.
Kos
21

Nie używam klas zagnieżdżonych, ale używam ich od czasu do czasu. Zwłaszcza gdy definiuję jakiś typ danych, a następnie chcę zdefiniować funktor STL zaprojektowany dla tego typu danych.

Rozważmy na przykład ogólną Fieldklasę, która ma numer identyfikacyjny, kod typu i nazwę pola. Jeśli chcę wyszukać jeden vectorz nich Fieldwedług numeru ID lub nazwy, mógłbym zbudować funktor, aby to zrobić:

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Następnie kod, który musi wyszukać te, Fieldmoże użyć matchzakresu w Fieldsamej klasie:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling
źródło
Dziękuję za świetny przykład i komentarze, choć nie jestem do końca świadomy funkcji STL. Zauważam, że konstruktory w match () są publiczne. Zakładam, że konstruktory nie zawsze muszą być publiczne, w takim przypadku nie można utworzyć instancji poza klasą.
okularach
1
@ użytkownik: W przypadku funktora STL konstruktor musi być publiczny.
John Dibling,
1
@Billy: Wciąż nie widzę żadnego konkretnego uzasadnienia, dlaczego zagnieżdżone klasy są złe.
John Dibling,
@John: Wszystkie wytyczne dotyczące stylu kodowania sprowadzają się do opinii. Wymieniłem kilka powodów w kilku komentarzach tutaj, z których wszystkie (moim zdaniem) są uzasadnione. Nie ma argumentu „faktycznego”, który mógłby zostać podany, o ile kod jest prawidłowy i nie wywołuje nieokreślonego zachowania. Myślę jednak, że przykład kodu, który tu umieściłeś, wskazuje na jeden z głównych powodów, dla których unikam klas zagnieżdżonych - mianowicie koliduje nazwa.
Billy ONeal
1
Oczywiście istnieją techniczne powody, aby preferować inline zamiast makr !!
Miles Rout
14

Można zaimplementować wzorzec Konstruktora z klasą zagnieżdżoną . Zwłaszcza w C ++ osobiście uważam, że jest semantycznie czystszy. Na przykład:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Zamiast:

class Product {}
class ProductBuilder {}
Yeo
źródło
Jasne, zadziała, jeśli jest tylko jedna kompilacja, ale stanie się nieprzyjemna, jeśli będzie potrzeba posiadania wielu konkretnych konstruktorów. Należy starannie podejmować decyzje projektowe :)
irsis