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.?
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.?
Odpowiedzi:
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.
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.
Lakos rozróżnia użycie klasy
Nigdy nie widziałem tego bardziej zwięźle :)
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
Ż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>;
X
i 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 ...
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.
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
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.
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.
I disagree with Luc Touraille's answer
Napisz 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.
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.
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.
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.
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.
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.
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.
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 #include
zwią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 #include
nagłówek do .cpp
plikó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 .h
plikach nagłówkowych .