Jste zde

Zajímavosti z C a C++: Proč nezahrnovat implementaci do hlavičkových souborů

V příspěvku se dozvíte, proč dávat přednost umisťování implementace do .c/.cpp souborů, nikoliv do hlavičkových souborů.

Úvod

Pro jednoduchost se zaměřím na C++, ale stejné argumenty platí i pro C. Pojďme na to! Dejme tomu, že máte třídu A, jež je tvořena soubory A.cpp a A.h:

// A.h
class A {
    // ...
};
 
// A.cpp
// Implementace třídy A

Při implementaci zjistíte, že by se vám hodila pomocná funkce, kterou byste využili např. ve více metodách. Jedna varianta, která vás ihned napadne, je vytvořit privátní statickou funkci ve třídě A:

// Varianta (1)
 
// A.h
class A {
    // ...
 
private:
    int helperFunc(/* parametry */);
};
 
// A.cpp
int A::helperFunc(/* parametry */) {
    // Implementace
}
 
// Implementace třídy A, která používá A::helperFunc

Druhá, méně zřejmá varianta, je použít klasickou "Céčkovou" funkci a umístit ji do A.cpp do anonymního prostoru jmen:

// Varianta (2)
 
// A.h
// Žádná změna
 
// A.cpp
namespace {
int helperFunc(/* parametry */) {
    // Implementace
}
} // Konec anonymního prostoru jmen
 
// Implementace třídy A, která používá helperFunc

V případě C ji lze definovat jako static (toto funguje i v C++ a je to ekvivalent anonymního prostoru jmen):

static int helperFunc(/* parametry */) {
    // Implementace
}

Nyní trochu odbočíme. Důvodem, proč se ona funkce ve variantě (2) dává do anonymního prostory prostoru jmen/označí se jako static je ten, že nyní má funkce interní linkování. Kdybychom to neudělali a někdo si vytvořil ve svém .cpp souboru funkci stejné signatury (zahrnuje i stejné jméno), tak by při linkování výsledného programu linker hlásil, že byla nalezena dvojí definice stejné funkce, což např. v případě C++ porušuje tzv. pravidlo jediné definice (ODR, z anglického "One Definition Rule"). U Céčka by to byl samozřejmě také problém. V případě interního linkování tento problém nenastane, protože ona funkce není veřejně viditelná mimo objektový soubor, kde se nachází.

Která varianta je lepší a proč?

Varianta (2), a to z následujícího důvodu. Projekty, které obsahují více zdrojových souborů, většinou pro urychlení vývoje používají tzv. inkrementální překlad. Funguje to tak, že pokud v projektu uděláte změnu a spustíte překlad, tak se nejdříve zjistí, které soubory byly změněny či které soubory závisí na změněných souborech, a přeloží se pouze ty. To může velmi snížit dobu nutnou pro sestavení celého projektu, protože se znovu nepřekládají soubory, které není nutné překládat. Může to vypadat klidně tak, že doba překladu celého projektu od začátku trvá 10 minut a inkrementální překlad při změněných dvou souborech bude trvat 10 sekund. A věřte nevěřte, když něco vyvíjíte, tak je to drastickým způsobem znát, zda je potřeba 10 minut či 10 sekund.

Uvažme nejdříve variantu (1). Pokud na onom hlavičkovém souboru A.h závisí dalších 20 souborů, tak v případě, kdy se rozhodnete změnit signaturu vaší funkce (např. přidáte další parametr), tak při následném překladu je nutné přeložit jak A.cpp, tak i oněch 20 souborů, které #includují A.h. Dále je třeba přeložit všechny soubory, které #includují .h soubory, které #includovaly A.h. A tak dále. Co tedy vidíme, tak je, že i změna funkce, která je pouze součástí implementace a nikoliv veřejného rozhraní, způsobila nutnost překládat soubory, kterých se tato změna nijak nedotkne. To je špatně. V ideálním případě by to mělo být tak, že nutnost znovupřeložení by měla vyvolat pouze změna, která se nějak dotkne kódu, který používá změněnou funkcionalitu. V případě varianty (1) se ale změnila jen privátně používaná funkce. To ale typicky překladový systém (např. GNU Make) nezjistí, protože ten se dívá pouze na datum změny souboru, nikoliv na rozdíly mezi změnami v souborech.

Uvažme nyní variantu (2). Ať už si libovolným způsobem změníme naši pomocnou funkci, tak při znovupřeložení se přeloží pouze A.cpp, nic dalšího. To je správně. Jak už jsem psal, tak změny implementace (tj. nikoliv rozhraní) by neměly způsobit nutnost překladu souborů, které využívají pouze definované rozhraní (public/protected složky tříd).

Kam tedy umisťovat interní záležitosti?

Moje doporučení tedy je, abyste to, co se dá bez znatelného snížení čitelnosti/rychlosti kódu umisťovali do .cpp souborů a nikoliv do .h souborů. Za prvé si tím potenciálně urychlíte překlad a za druhé budou vaše definice tříd čistější, protože nebudou obsahovat některé čistě implementační záležitosti. Typickými kandidáty pro přesun z .h souborů do .cpp souborů jsou privátní statické funkce (viz náš příklad), privátní typy, které nejsou využity v .h souboru či privátní metody, které nepracují se složkami třídy.

C++ nám bohužel hází klacky pod nohy, kdy v některých případech je nutné umisťovat implementaci do hlavičkových souborů. Jedná se např. o privátní datové složky třídy či metody, které s nimi manipulují. Toto se dá částečně řešit idiomem PIMPL (taky znám pod názvem opaque pointer), ale i ten má své pro a proti.

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.

Přidat komentář