Jezor
g/Programujmy

C++, "forward declarations"
Czy ktoś może mi to wytłumaczyć, bo ni cholery nie potrafię tego zrozumieć?
Myk polega na tym, żeby nie walić #include do każdego pliku, tylko napisać deklarację używanych w nim funkcji / klas. Ale nadal gdzieś musimy dać to #include mimo wszystko...

Załóżmy, że mamy takie przykładowe pliczki:

/ A.hpp /

class A {
public:
A();
B getB();
}

/ A.cpp /

#include "A.hpp"
A::A() {}
B A::getB() { B b; return b; }

/ B.hpp /

class B {
public:
B();
}

/ B.cpp /

#include "B.hpp"
B::B() {}

No i wszystko spoko, ale kiedy chcemy skorzystać z klasy A, musimy dać #include do pliku B.hpp:

/ main.cpp /

#include "A.hpp"
#include "B.hpp" // bez tego nie zadziała
int main () {
A a;
a.getB();
return 0;
}

Co nam to daje, że nie daliśmy tego wcześniej?
I jaki ma sens taki podział na pliki, skoro chcemy użyć tylko klasy A i nie powinno nas interesować, skąd A bierze B?

#
Jezor

sorry za formatowanie tekstu, ale na strimoidzie jest zjebane i nie da się kilku linijek dać jako kod...

#
szarak

@Jezor: wklej.org

#
Jezor

@szarak: http://pastebin.com/czcV66PG :<

#
onyx

@Jezor: To czego szukasz ściślej rzecz ujmując nazywa się "incomplete forward declarations". Tutaj [1] masz przedstawiony praktycznie dokładnie ten sam przykład, który opisałeś. W [2] oraz [3] (tutaj dokładniej) masz wyjaśnione kiedy możesz/nie możesz to stosować.

[1] http://www.adp-gmbh.ch/cpp/forward_decl.html
[2] http://stackoverflow.com/questions/553682/when-can-i-use-a-forward-declaration
[3] http://www.umich.edu/~eecs381/handouts/IncompleteDeclarations.pdf

#
Jezor

@onyx: dzięki Ci dobry człowiecze <3

Co do trzeciego linka:

an address is an address, regardless of what kind
of thing is at that address, and addresses are always the same size regardless of what they point t

Wskaźniki na funkcje / metody nie zawsze są takiego samego rozmiaru jak wskaźniki na zwykłe obiekty. Więc to trochę dziwne założenie dla mnie...

#
Taiga

@Jezor: spóźniłam się :(

#
onyx

@Jezor: Oczywiście masz rację, zapewne autor nie brał tego pod uwagę i myślał tylko o obiektach. Używając "incomplete forward declarations" na daną chwilę kompilatorowi wystarcza jedynie wiedza o rozmiarze wskaźnika. Jeśli w kodzie występuje wskaźnik do obiektu / funkcji / metody kompilator już wie z jakim wskaźnikiem ma do czynienia i dobierze odpowiedni rozmiar do niego, a jeśli nie wie to wyrazi swoje niezadowolenie w dosadny sposób :D

#
Jezor

@onyx: no ale właśnie, skoro nie może założyć z góry jak duży jest wskaźnik, to nadal mu nic nie daje... Dlatego nadal nie rozumiem, jak ma się zmniejszyć czas kompilacji, skoro jedyne co kompilatorowi zapewniamy to rozróżnienie czym jest dany symbol... Nie widzę żadnych plusów z używania niekompletnych deklaracji, za to widzę same minusy - np. niektóre #include w plikach .cpp zamiast .hpp, nie wiadomo gdzie ich szukać w kodzie...

#
onyx

@Jezor: Tzn. deklarując wcześniej class A; kompilator wie, że może mieć do czynienia z wskaźnikiem do niego i może dla niego przeznaczyć odpowiedni rozmiar. Zazwyczaj rozmiary wskaźników do różnych obiektów jest taki sam (nie mylić z rozmiarem samych obiektów), ale nie zawsze może to być zagwarantowane [1]. Przykładowo u mnie dla wskaźników do typów sizeof(char*) == sizeof(int*) == sizeof(double*) == 8 bajtów, z kolei dla wskaźników obiektów z przykładu z tego linku to sizeof(void (A::*)()) == sizeof(void (B::*)()) == sizeof(void (D::*)()) == 16 bajtów. Jak dokładnie sobie z tym radzi kompilator to nie umiem powiedzieć. Co do zmniejszenia czasu kompilacji, to używając niekompletnych deklaracji zmniejszamy ilość pojawiania się plików nagłówkowych (#include), dzięki czemu kompilator będzie je mniej przetwarzał. Jeśli chodzi o czytelność kodu to wydaje mi się, że jest to kwestia przyzwyczajenia. Dla mnie bardziej oczywiste jest, że w pliku .hpp tylko deklaruję to co później może być gdzieś przez coś używane (interfejs) i jak najmniej używam #include w pliku .hpp [2].

[1] http://stackoverflow.com/questions/399003/is-the-sizeofsome-pointer-always-equal-to-four
[2] http://programmers.stackexchange.com/questions/167723/what-should-and-what-shouldnt-be-in-a-header-file

#
Jezor

jak najmniej używam #include w pliku .hpp

@onyx: ale nadal czasem trzeba, np. w przypadku std::string. Więc jakieś tam #include w pliku nagłówkowym czasem się pojawi, a przez to robi się wg mnie drobny bałagan.

Zresztą popatrzmy na plik nagłówkowy ze standardowych bibliotek, np. iostream. Często są w nich #include dodawane...

#
onyx

@Jezor: Zaznaczyłem jak najmniej, a nie że wcale. Niezbędne pliki nagłówkowe muszą się pojawić, aby umożliwić poprawną kompilację. Jest to dobra praktyka, a nie nakaz. Akurat plik nagłówkowy iostream sam w sobie nic nie reprezentuje, zawiera jedynie odwołania do zewnętrznych obiektów, które znajdują się w innej jednostce kompilacji. Te obiekty są podstawowe i powszechnie znane, dlatego zrobiono oddzielny plik nagłówkowy tylko z nimi. Dlaczego iostream nie zawiera math.h? Często jak używam iostream również używam math.h. Strasznego mi psikusa zrobili!!! :D. Poważniej, przykładowo klasa A korzysta w swoich metodach z funkcji matematycznych (math.h), używając obiektu klasy A nie muszę o tym wiedzieć. Po co mam mieć w swoim pliku pośrednio #include <math.h> dzięki a.h, skoro ja używam tylko obiektów klasy A, nie potrzebuję żadnego pliku nagłówkowego biblioteki matematycznej. Tutaj [1] przykład oraz jego dobra analiza z klasami. Zmniejszając ilość zależność (ilości #include) minimalizujemy również niepotrzebne ich przetwarzanie w innych jednostkach kompilacji oraz zmniejszamy ewentualną rekompilacje w przypadku jakiejkolwiek zmiany w dany plik nagłówkowy [2]. Jak już wspomniałem jest to dobra praktyka, nie nakaz!!!

[1] http://www.eventhelix.com/RealtimeMantra/HeaderFileIncludePatterns.htm
[2] http://blog.knatten.org/2012/11/09/another-reason-to-avoid-includes-in-headers/

#
Jezor

@onyx: spokojnie, wszystko już rozumiem dzięki Twoim wyjaśnieniom i linkom. Martwi mnie tylko ta niekonsekwencja w tym, gdzie umieszczamy dyrektywy #include :)

#