Kiedy mogę skorzystać z deklaracji przekazania?

602

Szukam definicji, kiedy wolno mi wykonać deklarację przekazywania klasy w pliku nagłówkowym innej klasy:

Czy mogę to zrobić dla klasy podstawowej, dla klasy będącej członkiem, dla klasy przekazanej do funkcji członka przez odniesienie itp.?

Igor Oks
źródło
14
Desperacko chcę zmienić nazwę na „kiedy powinienem ”, a odpowiedzi odpowiednio zaktualizowane ...
deworde
12
@deworde Kiedy mówisz, kiedy „powinieneś”, pytasz o opinię.
AturSams
@deworde Rozumiem, że chcesz używać deklaracji przesyłania w dowolnym momencie, aby skrócić czas kompilacji i uniknąć cyklicznych odwołań. Jedynym wyjątkiem, jaki mogę wymyślić, jest to, że plik dołączania zawiera typedefs, w którym to przypadku istnieje kompromis między ponownym zdefiniowaniem typedef (i ryzykowanie jego zmiany) a dołączeniem całego pliku (wraz z jego rekursywnymi włączeniami).
Ohad Schneider,
@OhadSchneider Z praktycznego punktu widzenia nie jestem wielkim fanem nagłówków. ÷
deworde
w zasadzie zawsze wymagają podania innego nagłówka, aby ich użyć (przedni decl parametru konstruktora jest tutaj dużym winowajcą)
deworde

Odpowiedzi:

962

Postaw się w pozycji kompilatora: kiedy przekazujesz deklarację typu, kompilator wie tylko, że ten typ istnieje; nie wie nic o swojej wielkości, członkach ani metodach. Dlatego nazywa się to niekompletnym typem . Dlatego nie można użyć tego typu do zadeklarowania elementu członkowskiego lub klasy podstawowej, ponieważ kompilator musiałby znać układ typu.

Zakładając następującą deklarację terminową.

class X;

Oto, co możesz, a czego nie możesz zrobić.

Co możesz zrobić z niepełnym typem:

  • Zadeklaruj członka jako wskaźnik lub odniesienie do niekompletnego typu:

    class Foo {
        X *p;
        X &r;
    };
    
  • Zadeklaruj funkcje lub metody, które akceptują / zwracają niekompletne typy:

    void f1(X);
    X    f2();
    
  • Zdefiniuj funkcje lub metody, które akceptują / zwracają wskaźniki / odwołania do niekompletnego typu (ale bez użycia jego elementów):

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}
    

Czego nie możesz zrobić z niekompletnym typem:

  • Użyj go jako klasy podstawowej

    class Foo : X {} // compiler error!
  • Użyj go, aby zadeklarować członka:

    class Foo {
        X m; // compiler error!
    };
    
  • Zdefiniuj funkcje lub metody za pomocą tego typu

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Skorzystaj z jego metod lub pól, próbując wyłuskać zmienną o niepełnym typie

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    

Jeśli chodzi o szablony, nie ma bezwzględnej reguły: to, czy można użyć niepełnego typu jako parametru szablonu, zależy od sposobu, w jaki typ jest używany w szablonie.

Na przykład std::vector<T>wymaga , aby jego parametr był kompletnym typem, podczas gdy boost::container::vector<T>nie. Czasami pełny typ jest wymagany tylko w przypadku korzystania z niektórych funkcji składowych; tak jest na przykład w przypadkustd::unique_ptr<T> .

Dobrze udokumentowany szablon powinien wskazywać w swojej dokumentacji wszystkie wymagania dotyczące jego parametrów, w tym czy muszą być kompletnymi typami, czy nie.

Luc Touraille
źródło
4
Świetna odpowiedź, ale proszę zobaczyć moją poniżej dla punktu inżynierskiego, w którym się nie zgadzam. Krótko mówiąc, jeśli nie uwzględnisz nagłówków niekompletnych typów, które akceptujesz lub zwracasz, wymuszasz niewidoczną zależność od tego, czy konsument nagłówka musi wiedzieć, jakich innych potrzebuje.
Andy Dent,
2
@AndyDent: Prawda, ale konsument nagłówka musi tylko uwzględnić zależności, których faktycznie używa, więc jest to zgodne z zasadą C ++: „płacisz tylko za to, czego używasz”. Ale w rzeczywistości może być niewygodny dla użytkownika, który spodziewałby się, że nagłówek będzie samodzielny.
Luc Touraille,
8
Ten zestaw reguł ignoruje jeden bardzo ważny przypadek: potrzebujesz pełnego typu, aby utworzyć instancję większości szablonów w bibliotece standardowej. Należy zwrócić na to szczególną uwagę, ponieważ naruszenie reguły powoduje niezdefiniowane zachowanie i nie może powodować błędu kompilatora.
James Kanze
12
+1 za „postaw się w pozycji kompilatora”. Wyobrażam sobie, że „kompilator” ma wąsy.
PascalVKooten
3
@JesusChrist: Dokładnie: kiedy przekazujesz obiekt według wartości, kompilator musi znać jego rozmiar, aby wykonać odpowiednią operację na stosie; przy przekazywaniu wskaźnika lub odwołania kompilator nie potrzebuje rozmiaru ani układu obiektu, a jedynie rozmiaru adresu (tj. rozmiaru wskaźnika), który nie zależy od wskazanego typu.
Luc Touraille
45

Główną zasadą jest to, że można deklarować tylko klasy, których układ pamięci (a zatem funkcje składowe i elementy danych) nie muszą być znane w pliku, który deklarujesz dalej.

Wykluczałoby to klasy podstawowe i wszystko inne niż klasy używane przez odwołania i wskaźniki.

Timo Geusch
źródło
6
Prawie. Można również odnosić się do „zwykłych” (tj. Nie wskazujących / referencyjnych) typów niekompletnych jako parametrów lub typów zwracanych w prototypach funkcji.
j_random_hacker
Co z klasami, których chcę używać jako członków klasy, którą definiuję w pliku nagłówkowym? Czy mogę przekazać je dalej?
Igor Oks
1
Tak, ale w takim przypadku możesz użyć tylko odwołania lub wskaźnika do zadeklarowanej do przodu klasy. Niemniej jednak pozwala ci to mieć członków.
Reunanen
32

Lakos rozróżnia użycie klasy

  1. tylko w nazwie (dla której wystarczająca jest deklaracja forward) oraz
  2. in-size (dla którego potrzebna jest definicja klasy).

Nigdy nie widziałem tego bardziej zwięźle :)

Marc Mutz - mmutz
źródło
2
Co oznacza tylko nazwa?
Boon,
4
@Boon: czy mogę to powiedzieć ...? Jeśli używasz tylko klasy nazwę ?
Marc Mutz - mmutz
1
Plus jeden dla Lakos, Marc
mlvljr
28

Oprócz wskaźników i odniesień do niekompletnych typów można także deklarować prototypy funkcji, które określają parametry i / lub zwracają wartości, które są niekompletne. Nie można jednak zdefiniować funkcji mającej niekompletny parametr lub typ zwracany, chyba że jest to wskaźnik lub odwołanie.

Przykłady:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types
j_random_hacker
źródło
19

Żadna z dotychczasowych odpowiedzi nie opisuje, kiedy można użyć deklaracji forward szablonu klasy. A więc proszę bardzo.

Szablon klasy można przekazać deklarowany jako:

template <typename> struct X;

Po strukturze przyjętej odpowiedzi ,

Oto, co możesz, a czego nie możesz zrobić.

Co możesz zrobić z niepełnym typem:

  • Zadeklaruj element członkowski jako wskaźnik lub odwołanie do niekompletnego typu w innym szablonie klasy:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • Zadeklaruj członka jako wskaźnik lub odniesienie do jednej z jego niekompletnych instancji:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Deklaruj szablony funkcji lub szablony funkcji członka, które akceptują / zwracają typy niekompletne:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Zadeklaruj funkcje lub funkcje składowe, które akceptują / zwracają jedną z niepełnych instancji:

    void      f1(X<int>);
    X<int>    f2();
  • Zdefiniuj szablony funkcji lub szablony funkcji członka, które akceptują / zwracają wskaźniki / odwołania do niekompletnego typu (ale bez użycia jego elementów):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • Zdefiniuj funkcje lub metody, które akceptują / zwracają wskaźniki / odniesienia do jednej z jej niekompletnych instancji (ale bez użycia jej elementów):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Użyj go jako klasy podstawowej innej klasy szablonów

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Użyj go, aby zadeklarować członka innego szablonu klasy:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Zdefiniuj szablony funkcji lub metody za pomocą tego typu

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

Czego nie możesz zrobić z niekompletnym typem:

  • Użyj jednej z jego instancji jako klasy podstawowej

    class Foo : X<int> {} // compiler error!
  • Użyj jednej z jego instancji, aby zadeklarować członka:

    class Foo {
        X<int> m; // compiler error!
    };
  • Zdefiniuj funkcje lub metody za pomocą jednej z jego instancji

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Użyj metod lub pól jednej z jego instancji, próbując w rzeczywistości wyłuskać zmienną o niepełnym typie

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Utwórz jawne instancje szablonu klasy

    template struct X<int>;
R Sahu
źródło
2
„Żadna z dotychczasowych odpowiedzi nie opisuje, kiedy można przekazać deklarację szablonu klasy.” Czy nie jest tak po prostu dlatego, że semantyka Xi X<int>są dokładnie takie same, a tylko składnia deklarująca do przodu różni się w jakikolwiek istotny sposób, przy czym wszystkie z wyjątkiem 1 linii twojej odpowiedzi to tylko wzięcie Luca i s/X/X<int>/g? Czy to naprawdę potrzebne? A może przeoczyłem drobny szczegół, który jest inny? Jest to możliwe, ale kilka razy porównałem wizualnie i nie widzę żadnych ...
underscore_d
Dziękuję Ci! Ta edycja dodaje ton cennych informacji. Będę musiał go przeczytać kilka razy, aby w pełni go zrozumieć ... lub może zastosować często lepszą taktykę oczekiwania, aż okropnie pomieszam się z prawdziwym kodem i wrócę tutaj! Podejrzewam, że będę mógł to wykorzystać do zmniejszenia zależności w różnych miejscach.
underscore_d
4

W pliku, w którym używasz tylko wskaźnika lub odwołania do klasy. Żadna funkcja członka / członka nie powinna być wywoływana przez ten wskaźnik / odwołanie.

z class Foo;// deklaracją przekazania

Możemy zadeklarować członków danych typu Foo * lub Foo &.

Możemy deklarować (ale nie definiować) funkcje za pomocą argumentów i / lub zwracać wartości typu Foo.

Możemy zadeklarować członków danych statycznych typu Foo. Jest tak, ponieważ elementy danych statycznych są zdefiniowane poza definicją klasy.

tak
źródło
4

Piszę to jako osobną odpowiedź, a nie tylko komentarz, ponieważ nie zgadzam się z odpowiedzią Luca Touraille'a, nie ze względu na legalność, ale ze względu na solidne oprogramowanie i niebezpieczeństwo błędnej interpretacji.

W szczególności mam problem z dorozumianą umową dotyczącą tego, czego oczekują użytkownicy interfejsu.

Jeśli zwracasz lub akceptujesz typy referencji, to po prostu mówisz, że mogą one przejść przez wskaźnik lub referencję, które z kolei mogą znać tylko poprzez przekazanie dalej.

Zwracając niekompletny typ, X f2();mówisz, że twoja osoba dzwoniąca musi mieć pełną specyfikację typu X. Potrzebują jej do stworzenia LHS lub obiektu tymczasowego w miejscu połączenia.

Podobnie, jeśli zaakceptujesz niekompletny typ, program wywołujący musi zbudować obiekt, który jest parametrem. Nawet jeśli obiekt został zwrócony jako inny niekompletny typ funkcji, strona wywołująca wymaga pełnej deklaracji. to znaczy:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

Myślę, że istnieje ważna zasada, że ​​nagłówek powinien dostarczać wystarczającą ilość informacji, aby z niego korzystać, bez zależności wymagającej innych nagłówków. Oznacza to, że nagłówek powinien być w stanie zostać włączony do jednostki kompilacyjnej bez powodowania błędu kompilatora podczas korzystania z deklarowanych funkcji.

Z wyjątkiem

  1. Jeśli pożądana jest ta zależność zewnętrzna . Zamiast korzystać z kompilacji warunkowej możesz mieć dobrze udokumentowane wymaganie, aby podawali swój własny nagłówek deklarujący X. Jest to alternatywa dla używania #ifdefs i może być użytecznym sposobem na wprowadzenie próbnych lub innych wariantów.

  2. Ważnym rozróżnieniem są niektóre techniki szablonów, w których wyraźnie NIE oczekuje się ich tworzenia, wspomniane tylko po to, aby ktoś nie był ze mną wredny.

Andy Dent
źródło
„Myślę, że istnieje ważna zasada, że ​​nagłówek powinien dostarczać wystarczającą ilość informacji, aby z niego korzystać, bez zależności wymagającej innych nagłówków”. - inny problem jest wspomniany w komentarzu Adriana McCarthy'ego na temat odpowiedzi Naveen. To dobry powód, aby nie stosować się do zasady „należy podać wystarczającą ilość informacji do użycia”, nawet w przypadku typów obecnie nieobjętych szablonem.
Tony Delroy,
3
Mówisz o tym, kiedy powinieneś (lub nie powinieneś) stosować deklaracji forward. Jednak nie o to chodzi w tym pytaniu. Chodzi o poznanie technicznych możliwości, gdy (na przykład) chcemy przełamać problem zależności cyklicznej.
JonnyJD
1
I disagree with Luc Touraille's answerNapisz więc komentarz, w tym link do posta na blogu, jeśli potrzebujesz długości. To nie odpowiada na zadane pytanie. Gdyby wszyscy zastanawiali się, jak działa X, uzasadnione odpowiedzi nie zgadzają się z tym, że X robi to, lub debatują nad granicami, w których powinniśmy ograniczać naszą swobodę korzystania z X - prawie nie mielibyśmy prawdziwych odpowiedzi.
underscore_d
3

Ogólna zasada, której przestrzegam, nie zawiera plików nagłówkowych, chyba że muszę. Więc jeśli nie przechowuję obiektu klasy jako zmiennej członka mojej klasy, nie dołączę go, użyję tylko deklaracji forward.

Naveen
źródło
2
To łamie enkapsulację i powoduje, że kod jest kruchy. Aby to zrobić, musisz wiedzieć, czy typem jest typedef, czy klasa dla szablonu klasy z domyślnymi parametrami szablonu, a jeśli implementacja kiedykolwiek się zmieni, musisz zaktualizować zawsze miejsce, w którym użyłeś deklaracji przesyłania dalej.
Adrian McCarthy
@AdrianMcCarthy ma rację, a rozsądnym rozwiązaniem jest posiadanie nagłówka deklaracji przesyłania zawartego w nagłówku, którego treść deklaruje przekazywanie, który powinien być własnością / utrzymywany / wysyłany przez każdego, kto jest właścicielem tego nagłówka. Na przykład: nagłówek biblioteki Standard iosfwd, który zawiera deklaracje forward zawartości iostream.
Tony Delroy,
3

Tak długo, jak nie potrzebujesz definicji (myśl wskaźniki i referencje), możesz uniknąć deklaracji forward. Właśnie dlatego najczęściej można je zobaczyć w nagłówkach, podczas gdy pliki implementacyjne zwykle ściągają nagłówek odpowiedniej definicji.

bezpośrednio
źródło
0

Zazwyczaj będziesz chciał użyć deklaracji przekazywania w pliku nagłówkowym klas, gdy chcesz użyć innego typu (klasy) jako członka klasy. Nie można używać metod klas zadeklarowanych w przód w pliku nagłówkowym, ponieważ C ++ nie zna jeszcze definicji tej klasy w tym momencie. To logika, którą musisz przenieść do plików .cpp, ale jeśli używasz funkcji szablonów, powinieneś zredukować je tylko do części, która używa szablonu i przenieść tę funkcję do nagłówka.

Patrick Glandien
źródło
To nie ma sensu. Nie można mieć członka niepełnego typu. Każda deklaracja klasy musi zawierać wszystko, co wszyscy użytkownicy powinni wiedzieć o jej rozmiarze i układzie. Jego rozmiar obejmuje rozmiary wszystkich elementów niestatycznych. Przekazywanie członka dalej nie pozostawia użytkownikom pojęcia o jego wielkości.
underscore_d
0

Weźmy pod uwagę, że deklaracja przekazania spowoduje kompilację kodu (tworzony jest obiekt obj). Łączenie (tworzenie exe) nie powiedzie się, dopóki nie zostaną znalezione definicje.

Sesh
źródło
2
Dlaczego 2 osoby głosowały na to? Nie mówisz o tym, o czym mówi pytanie. Masz na myśli normalną - nie przekazywaną dalej - deklarację funkcji . Pytanie dotyczy terminowej deklaracji klas . Jak powiedziałeś: „deklaracja przekazania spowoduje, że Twój kod się skompiluje”, zrób mi przysługę: skompiluj class A; class B { A a; }; int main(){}i daj mi znać, jak to działa . Oczywiście, że się nie skompiluje. Wszystkie prawidłowe odpowiedzi tutaj wyjaśniają powód i dokładne, ograniczone konteksty, w których deklaracja forward jest ważna. Zamiast tego napisałeś o czymś zupełnie innym.
underscore_d
0

Chcę tylko dodać jedną ważną rzecz, którą możesz zrobić z przekazaną klasą niewymienioną w odpowiedzi Luca Touraille'a.

Co możesz zrobić z niepełnym typem:

Zdefiniuj funkcje lub metody, które akceptują / zwracają wskaźniki / referencje do niekompletnego typu i przekazują te wskaźniki / referencje do innej funkcji.

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Moduł może przekazać obiekt deklarowanej do przodu klasy do innego modułu.

Miły mężczyzna
źródło
„klasa przekazana” i „klasa zadeklarowana do przodu” mogą się mylić, odnosząc się do dwóch bardzo różnych rzeczy. To, co napisałeś, wynika bezpośrednio z pojęć ukrytych w odpowiedzi Luca, więc chociaż byłby to dobry komentarz z jawnym wyjaśnieniem, nie jestem pewien, czy uzasadnia odpowiedź.
underscore_d
0

Jako, że Luc Touraille już bardzo dobrze wyjaśnił, gdzie używać, a nie używać terminowej deklaracji klasy.

Dodam tylko do tego, dlaczego musimy go używać.

W miarę możliwości powinniśmy używać deklaracji Forward, aby uniknąć niepożądanego wstrzykiwania zależności.

W #includezwiązku z tym, że pliki nagłówkowe są dodawane do wielu plików, jeśli dodamy nagłówek do innego pliku nagłówka, doda niechciane wstrzyknięcie zależności w różnych częściach kodu źródłowego, którego można uniknąć, dodając #includenagłówek do .cppplików tam, gdzie to możliwe, zamiast dodawać do innego pliku nagłówka i w miarę możliwości używaj deklaracji przekazywania klasy w .hplikach nagłówkowych .

A 786
źródło