Wersja artykułu 1.2
Język C++, choć wyraźnie pozostaje w tyle za rozwojem nowoczesnych metod i języków programowania takich jak C# czy Java, w wielu dziedzinach wciąż jest najważniejszym językiem programowania. Jedną z jego wad jest konieczność stosowania plików nagłówkowych, a w konsekwencji bardzo długi czas kompilacji programów.
Precompiled Headers to mechanizm, który małym nakładem pracy pozwala wielokrotnie przyspieszyć kompilację. Posiada go wiele kompilatorów. Ja tutaj skupię się na Visual C++ 2005. W innych wersjach kompilatora Microsoftu powinno wyglądać to podobnie. Dla innych kompilatorów, np. g++, będziesz musiał poszukać innego artykułu.
Precompiled Headers to ogólny mechanizm i można używać go na różne sposoby. Ja zamiast opisywać go abstrakcyjnie postaram się pokazać, jak w praktyce można go wykorzystać w przykładowym projekcie w Visual C++.
Zastosowanie Precompiled Headers można streścić w następujących 6 krokach. Opiszę je dokładnie poniżej.
Załóżmy, że mamy projekt programu w C++ złożony z kilku plików źródłowych cpp i nagłówkowych h. Chcemy zastosować do niego mechanizm Precompiled Headers, żeby przyspieszyć jego kompilację. Oto co dokładnie musisz zrobić:
"Precompiled header", czyli nagłówek prekompilowany, jak sama nazwa wskazuje, musi być plikiem nagłówkowym. Moglibyśmy uczynić nim jeden z istniejących w naszym projekcie nagłówków, ale ja proponuję utworzyć nowy. W tym celu kliknij File > New > File..., wybierz "Header File (.h)" i jako nazwę wpisz "PCH.h". Nazwa jest oczywiście dowolna, to tylko moja propozycja.
W jego treści może się znaleźć cokolwiek - bezpośrednio deklaracje funkcji, zmiennych i klas, ale w szczególności include-y innych plików nagłówkowych. Ten plik będzie prekompilowany i każdy plik źródłowy cpp, który go użyje, będzie się kompilował dużo szybciej. Co powinniśmy do niego wpisać?
Należy w nim włączyć inne pliki nagłówkowe - wszystkie te, które chcesz mieć do dyspozycji we wszystkich lub w większości plików źródłowych swojego projektu, bo często z nich korzystasz w różnych miejscach i traktujesz je jako podstawę, do której potrzebujesz dostępu zawsze i wszędzie. Chodzi szczególnie o biblioteki systemowe, które nierzadko liczą sobie dziesiątki albo setki tysięcy linii kodu i dotychczas musiały przetwarzane od początku przy kompilacji każdego pliku cpp. Mogą to być na przykład:
Przykładowy plik PCH.h może wyglądać tak:
Sam plik nagłówkowy to za mało. Pliki h są tak naprawdę traktowane przez kompilator jak powietrze - jakby ich w ogóle nie było. Mogą mieć dowolne rozszerzenie, leżeć w dowolnych katalogach i nie należeć nawet do projektu - i tak zadziałają poprawnie, bo są po prostu dosłownie, tekstowo włączane do plików źródłowych cpp przez preprocesor podczas kompilacji.
Dlatego do naszego prekompilowanego nagłówka potrzebujemy odpowiadającego mu pliku źródłowego. W tym celu kliknij File > New > File... i wybierz "C++ File (.cpp)" oraz wpisz nazwę "PCH.cpp". Ten plik nie będzie robił nic szczególnego, a jedynie posłuży nam do wygenerowania prekompilowanego nagłówka, który potem zostanie użyty przez pozostałe pliki źródłowe projektu. Dlatego jako jego treść wpisz następującą, jedną jedyną linijkę:
Czas na pogrzebanie w opcjach projektu. Musimy powiedzieć kompilatorowi, że ma skorzystać z mechanizmu Precompiled Headers i wskazać odpowiedni plik. Kliknij prawym klawiszem na projekt i z menu kontekstowego wybierz Properties.
Pokaże się okienko opcji projektu. W nim, w drzewku po lewej stronie wybierz gałąź Configuration Properties > C/C++ > Precompiled Headers. Po prawej stronie przestaw:
Co tu się stało? Tak naprawdę nie ma czegoś takiego jak ustawienie Precompiled Headers dla całego projektu. Przestawiając te opcje w ustawieniach projektu przestawiliśmy opcje, które będą stosowane domyślnie dla każdego pliku źródłowego cpp w naszym projekcie - o to chodziło. Każdy plik źródłowy ma korzystać z tego nagłówka do przyspieszenia kompilacji i właśnie poinformowaliśmy o tym kompilator.
Nagłówek prekompilowany tak naprawdę przybierze postać ogromnego, wielomegabajtowego pliku o rozszerzeniu PCH umieszczonego pośród plików tymczasowych kompilacji obok plików ILK, PDB, RES, OBJ i innych tym podobnych. Żeby można było z niego korzystać, musi jednak najpierw powstać. Dlatego jednemu z plików źródłowych naszego projektu musimy nakazać utworzenie prekompilowanego nagłówka. Właśnie po to powstał PCH.cpp.
Podobnie jak wyżej, kliknij prawym klawiszem tym razem na plik PCH.cpp i z menu wybierz Properties.
W oknie dialogowym, które się otworzy, w gałęzi Configuration Properties > C/C++ > Precompiled Headers przestaw opcję Create/Use Precompiled Header na: Create Precompiled Header (/Yc).
Przestawienie tych opcji nie załatwi jednak za nas w automagiczny sposób przyspieszenia kompilacji. To do ciebie - programisty - należy dokończenie dzieła poprzez prawdziwe włączenie tego prekompilowanego nagłówka do każdego pliku źródłowego cpp swojego projektu, w którym zdecydowałeś się go używać. W tym celu, W KAŻDYM PLIKU ŹRÓDŁOWYM CPP, NA SAMYM POCZĄTKU, MUSISZ WŁĄCZYĆ NAGLÓWEK PCH.h.
Jeśli o tym zapomnisz, otrzymasz błąd kompilacji o następującej treści: fatal error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "PCH.h"' to your source?
Być może z jakiś powodów nie chcesz albo nie możesz włączyć prekompilowanego nagłówka PCH.h na początku jednego ze swoich plików źródłowych cpp w projekcie. Na przykład kiedy jest to zewnętrzna biblioteka autorstwa innego programisty, która nie jest związana z resztą twojego projektu, ale zdecydowałeś się włączyć ją bezpośrednio do projektu i nie chcesz jej modyfikować. Możesz w takim przypadku zrezygnować z korzystania z mechanizmu Precompiled Headers podczas jego kompilacji. Musisz jednak poinformować o tym kompilator, by nie szukał tam włączenia PCH.h i nie wyświetlił błędu takiego jak zacytowany powyżej.
W tym celu kliknij prawym klawiszem na taki plik cpp i z menu kontekstowego wybierz Properties. Następnie, w oknie dialogowym które się pokaże w gałęzi Configuration Properties > C/C++ > Precompiled Headers przestaw opcję Create/Use Precompiled Header na: Not Using Precompiled Headers.
Jakie problemy mogą się pojawić podczas korzystania z mechanizmu Precompiled Headers? Po pierwsze, zmiana czegokolwiek w którymś z plików włączanych przez nasz nagłówek prekompilowany wymusza rekompilację wszystkich korzystających z niego plików, a więc praktycznie całego projektu. Jednak czy to na pewno wada? W końcu założeniem było, że do PCH.h wpisujemy te nagłówki, których i tak chcemy używać w całym projekcie. Poza tym, zaleca się wpisywać tam nagłówki modułów, które są już stabilne i nie zmienia się ich zbyt często (szczególnie bibliotek systemowych), a moduły, które są w takcie pisania lub modyfikacji włączać we własnym zakresie tam gdzie to potrzebne.
Po drugie, problemy mogą się pojawić jeśli pliki swojego projektu masz zorganizowane we więcej niż pojedynczy katalog. Kompilator nie jest świadomy położenia tych plików względem siebie i nie zrozumie, że włączony w jednym pliku cpp "PCH.h" to ten sam plik, co włączony w drugim "../PCH.h" i w trzecim "Common/PCH.h", nawet jeśli te ścieżki wskazują poprawną lokalizację względem danego pliku cpp. Kompilator w takim przypadku zgłosi błąd, że nie znalazł prekompilowanego nagłówka włączonego dosłownie taki sposób, jaki został zadeklarowany w opcjach projektu.
Co wtedy zrobić? Rozwiązaniem jest niekorzystanie ze ścieżek względnych podczas włączania plików w swoim projekcie, a jedynie specyfikowanie ich nazw i dodanie wszystkich podkatalogów projektu do ścieżek, w których kompilator ma poszukiwać włączanych plików. W tym celu kliknij prawym klawiszem na projekt, wybierz z menu Properties, w oknie właściwości projektu zaznacz gałąź Configuration Properties > C/C++ > General i wypełnij odpowiednio opcję Additional Include Directories.
Za zwrócenie mi uwagi na ten problem, a raczej za uświadomienie, że jego rozwiązanie nie jest tak oczywiste jak mi się wydawało, podziękowania należą się koledze o pseudonimie Koshmaar (wątek forum).
Rodzi się pytanie: Jeśli w pliku nagłówkowym używam używamy czegoś z jakiejś biblioteki, np. klasy std::vector z nagłówka <vector>, to wypadałoby włączyć ten nagłówek wewnątrz naszego pliku nagłówkowego. Tak nakazywałyby zasady poprawnego programowania w C++. Wówczas i tak włączamy każdy używany nagłówek tam gdzie to potrzebne i cały mechanizm Precompiled Headers wydaje się niepotrzebny. Jak to rozwiązać?
Rozwiązaniem jest następujący tok myślenia: Skoro zakładamy, że PCH.h włącza <vector>, a my włączamy nasz PCH.h w każdym pliku cpp, to możemy w tych plikach, a nawet w naszych nagłówkach h używać klasy std::vector bez dodatkowego włączania <vector>. Na przykład:
// ---- PLIK: SrobkaDoSilnika.h: class Strobka { private: std::vector<float> m_WektorFloatow; public: void Wypelnij(); }; // ---- PLIK: SrobkaDoSilnika.cpp: #include "pch.h" #include "SrobkaDoSilnika.h" void Srobka::Wypelnij() { m_WektorFloatow.push_back(666); } // ---- PLIK: TlokDoSilnika.cpp: #include "pch.h" #include "SrobkaDoSilnika.h" #include "TlokDoSilnika.h" // ...
To zadziała, bo każdy plik, który włącza StobkaDoSilnika.h włącza wcześniej pch.h więc mamy pewność, że std::vector jest mu znany.
Efekt jest prosty. Kompiluje się najpierw ten plik, który tworzy prekompilowany nagłówek, a dopiero potem te, które z niego korzystają - już bardzo szybko.
Problem jest dużo szerszy, a mechanizm Precompiled Headers bardziej elastyczny, niż to pokazuje przedstawiony tutaj przypadek. Moim celem było jednak ukazanie, jak można wykorzystać go w praktyce przedstawiając przykład użycia. Jeśli chcesz, możesz w MSDN lub na Google poszukać więcej informacji, jak choćby przełączników pozwalających na korzystanie z Precompiled Headers podczas uruchamiania kompilatora z parametrami z poziomu wiersza poleceń.
Precompiled Headers to funkcja kompilatora, która, kiedy użyta poprawnie, nie stwarza żadnych problemów, a wielokrotnie przyspiesza kompilację. To bardzo ważne - tym ważniejsze, im większy jest twój projekt. Jest przy tym bardzo prosta do zastosowania. Wystarczy zastosować opisane wyżej 6 kroków. Z pewnością wielu programistów by ją stosowało, gdyby tylko wiedziało o jej istnieniu. Dlatego moim celem było przedstawienie w tym artykule w możliwie najbardziej przystępny sposób, jak zacząć używać Precompiled Headers.