Minule jsme si ukázali, proč nezahrnovat implementaci do hlavičkových souborů. Dnes se podíváme na to, jak urychlit překlad použitím tzv. dopředných deklarací.
Úvod
Pro jednoduchost se zaměřím na C++, ale podobné argumenty platí i pro C.
Dejme tomu, že jsme vytvořili třídu Parser
(syntaktický analyzátor), která interně využívá instanci třídy Lexer
(lexikální analyzátor), resp. ukazatel na ni. Ta je předaná do konstruktoru. Definice třídy Parser
je rozdělena do dvou souborů, Parser.cpp
a Parser.h
:
// Parser.h class Parser { public: Parser(Lexer *lexer); // Veřejné rozhraní třídy Parser. // ... private: Lexer *lexer; }; // Parser.cpp #include "Parser.h" Parser::Parser(Lexer *lexer): lexer(lexer) { // ... } // Definice ostatních metod. // ...
Dále mějme aplikaci, která třídu Parser
vytváří a využívá. Pokud bychom kód nechali takto, tak nám překladač při překladu modulu Parser
zahlásí chybu, že byl použit neznámý identifikátor Lexer
. Většina programátorů to vyřeší tak, že do Parser.h
vloží #include Lexer.h
:
// Parser.h #include "Lexer.h" // Zbytek souboru je beze změny. // Parser.cpp // Beze změny.
Překlad již projde, což je hlavní. Ve zbytku příspěvku bych chtěl ale ukázat alternativu, která vám umožní v mnoha situacích urychlit překlad vašeho projektu bez újmy na udržovatelnosti/čitelnosti.
Dopředné deklarace
V C++ (i C) lze využít tzv. dopředných deklarací (angl. forward declaration). Dopřednou deklarací dáte překladači vědět, že daný identifikátor je určitého typu (tudíž jej bude znát), ale nedáte překladači vědět některé detaily, např. jak je onen objekt definován či jaká je jeho velikost. Pokud chceme napsat dopřednou deklaraci třídy, tak to uděláme takto:
class X;
kde X
je název oné třídy. S takto deklarovanou třídu pak můžete dělat následující (převzato odtud):
- Deklarovat datovou složku třídy jako ukazatel na instanci dané třídy:
class SomeClass { X *pt; X &pt; };
- Deklarovat funkce/metody, které mají jako parametr či vrací instanci oné třídy:
void f1(X); X f2();
- Definovat funkce/metody, které mají jako parametr či vrací ukazatel na instanci oné třídy:
void f3(X*, X&) {} X& f4() {} X* f5() {}
Co dělat např. nemůžete, tak je použít onu třídu jako bázovou třídu, deklarovat datovou složku třídy onoho typu, definovat funkce, které onen typ používají přímo (tedy nikoliv přes ukazatel) či přistupovat přes ukazatel na dopředně deklarovanou třídu (např. že zavoláte její metodu). Pro příklady mrkněte zde.
Co se týče našeho příkladu z úvodu, tak ona alternativa k #include "Lexer.h"
je právě použít dopřednou deklaraci:
// Parser.h class Lexer; // <-- dopředná deklarace class Parser { public: Parser(Lexer *lexer); // Veřejné rozhraní třídy Parser. // ... private: Lexer *lexer; }; // Parser.cpp #include "Parser.h" #include "Lexer.h" // <-- přesunuto z Parser.h Parser::Parser(Lexer *lexer): lexer(lexer) { // ... } // Definice ostatních metod. // ...
Samotný #include
hlavičkového souboru Lexer.h
se nám pak přesune do Parser.cpp
, protože v Parser.h
jej nebudeme potřebovat.
Moc tomu nerozumím. Jakou to má výhodu?
Dopředné deklarace mají různé výhody (např. při vytváření tříd, kde jednu používá druhou a druhá zase tu první a jsou v různých hlavičkových souborech). Já se zaměřím jen na jednu, která se váže na náš příklad: urychlení překladu. Toto urychlení lze pozorovat ve dvou případech:
- Při změnách v
Lexer.h
. Dejme tomu, že dojde ke změně vLexer.h
(kromě změny názvu třídy, samozřejmě). V první variantě se nám musí znovu přeložitParser.cpp
a každý soubor, který (ať už přímo či nepřímo)#includ
ujeLexer.h
aParser.h
. To je proto, žeParser.h
#includ
ujeLexer.h
. Je to podobná situace, jako u minulého příspěvku. U druhé varianty, která využívá dopřednou deklaraci, se přeloží pouzeParser.cpp
a každý soubor, který#includuje
Lexer.h
. Všimněte si, že už není nutné znovu překládat každý soubor, který#includ
ujeParser.h
. Proč by uživatelé, kteří využívajíParser
měli být nuceni jej znovu překládat, když se změnilo něco, co je využíváno pouze interně? Neměli. Právě proto byste měli využívat dopředné deklarace. Čím větší máte projekt, tím více to oceníte. Lexer.h
#includ
uje mnoho rozsáhlých souborů. Překladač při překladu modulu musí vložit těla všech#includ
ovaných souborů, což znamená jejich načtení z disku (vstupně/výstupní operace jsou pomalé), kontrolu, zda už jej nevložil (s využitím tzv. header guards) a jeho zpracování (lexikální analýza, syntaktická analýza atd.). Čím více takových souborů je, tím je samozřejmě delší doba překladu. Pokud snížíte počet#include
, snížíte tím i dobu překladu.
Co doporučuješ?
Předtím, než bezmyšlenkovitě provedete přidání #include "XXX.h"
do zdrojáku se vždy zamyslete nad tím, zda by nestačilo použít jen dopřednou deklaraci. Jsou situace, kde by to způsobilo více škody než užitku a ve kterých by to ani nešlo. O těch snad napíši někdy jindy. Ve všech ostatních případech ale doporučuji #include
nahradit dopřednou deklarací.
Shrnutí
Abychom si shrnuli tento a minulý příspěvek, pro urychlení překladu doporučuji:
- implementační detaily vkládat do
.cpp
souboru, nikoliv do.h
souboru, - místo
#include
využít dopředné deklarace (tam, kde to jde a dává to smysl).
Další čtení
Pokud by vás zajímala další diskuse a rady na toto téma, určitě mrkněte do zdrojů uvedených níže.
- wikipedia.org - Forward declaration
- stackoverflow.com - When to use forward declaration?
- stackoverflow.com - Why is including a header file such an evil thing?
- akhodakivskiy.github.io - Forward Declaration and Private Implementation in C++ (velmi pěkný článek)
- chromium.org - Forward declare classes instead of including headers
- www-subatech.in2p3.fr - C++ tip: Use forward declarations when possible
- programmers.stackexchange.com - Forward declaration vs include
- developingthefuture.net - Forward Declarations in C++
- gotw.ca - Forward Declarations
- thecplusplusblog.blogspot.com - Why you should use forward declarations