Pętle w include (C++)
Mechanizm włączania plików nagłówkowych jest wskazywany przez wielu programistów jako największa wada C++. Świadczy o tym chociażby fakt, że w nowocześniejszych językach programowania twórcy tychże języków starają się go całkowicie wyeliminować.
Tym niemniej, jak na razie, C++ wydaje się najlepszym językiem do tworzenia wydajnych aplikacji. Dzięki swojej uniwersalności ma wielu zwolenników (do których się bez wątpienia zaliczam) i pozostaje najpopularniejszym językiem programowania. Nie pozostaje więc nic innego jak nauczyć się ten mechanizm efektywnie wykorzystywać.
Źle / dobrze zaprojektowany układ plików nagłówkowych
Zanim przejdę do meritum sprawy, odpowiemy sobie na pytanie: "Co to znaczy, że układ plików nagłówkowych w projekcie jest źle lub dobrze zaprojektowany?". Odpowiedź jest stosunkowo prosta. Układ jest dobrze zaprojektowany jeśli spełnia dwa warunki:
- zmiana kolejności włączania dowolnych plików nagłówkowych nie powoduje, że program przestaje się kompilować,
- można w dowolnym momencie dołączyć dowolny plik nagłówkowy i program nadal się kompiluje.
Jeśli nasz projekt ma drzewiastą strukturę zależności klas, spełnienie powyższych warunków nie jest trudne. Problem pojawia się gdy w projekcie pojawiają się pętle.
Na czym polega problem "pętli"
Dość łatwo wyobrazić sobie sytuację, kiedy w naszym programie pojawiają się pętle w referencjach. Najprostszy przykład to drzewo dwukierunkowe, czyli takie, w którym "rodzic" zawiera wskaźniki na "dzieci", zaś każde dziecko ma wskaźnik na swojego "rodzica". Często spotyka się również bardziej skomplikowane pętle (o większych oczkach, czyli np.: klasa A wymaga włączenia definicji klasy B, klasa B wymaga włączenia klasy C, a klasa C - klasy A).
Gdy projekt się rozrasta, pętli staje się coraz więcej, zazębiają się i okazuje się, że nie można już dołączyć nowej klasy korzystającej z poprzednich, bez każdorazowego rozwiązania wielu problemów z włączaniem plików nagłówkowych... Dochodzimy do wniosku, że problem należy rozwiązać globalnie.
Globalne rozwiązanie problemu
Pierwsze rozwiązanie, które wtedy przychodzi do głowy jest proste: "Zróbmy jeden plik nagłówkowy, który będzie włączał poszczególne komponenty w odpowiedniej kolejności, w ten sposób unikniemy horroru". I faktycznie unikniemy, ale w zamian wpakujemy się w inny, równie dokuczliwy: Niewielka zmiana w dowolnym pliku nagłówkowym spowoduje konieczność przekompilowania całego projektu. Nie trzeba chyba nikomu tłumaczyć jak znacząco wpłynie to na wydajność procesu tworzenia oprogramowania.
Znacznie lepsze rozwiązanie
Na początku zastanówmy się i poeksperymentujmy, kiedy tak naprawdę zachodzi potrzeba dołączenia pliku nagłówkowego z definicją klasy, a kiedy nie:
| Operacja | Czy trzeba włączyć definicję klasy? |
|---|---|
| Dziedziczenie | tak |
| Zawieranie | tak |
| Zawieranie wskaźnika | nie wystarczy zdeklarowanie klasy poleceniem class nazwa; |
| Wywołanie metody | tak |
| Odwołanie do składowej | tak |
Analiza powyższej tabelki prowadzi do następujących wniosków:
- Definicję klasy A należy włączyć w headerze klasy B
jeśli:
- B dziedziczy po A
- B zawiera w sobie A
- Definicji klasy A nie należy włączać w headerze
klasy B jeśli:
- klasa B zawiera wskaźnik na A - wtedy wystarczy
przed definicją klasy B dopisać linijkę
class A;
- klasa B zawiera wskaźnik na A - wtedy wystarczy
przed definicją klasy B dopisać linijkę
- Nie wolno umieszczać w headerach wywołań metod i odwołań do obiektów należących do innych klas, chyba, że klasa taka jest zawarta w deklarowanej. Inne wywołania należy przenieść do pliku .cpp, w którym bez obaw będziemy mogli włączyć potrzebny plik nagłówkowy.
Stosując się do tych prostych reguł stworzymy układ headerów, który zawsze będzie "dobrze zaprojektowany", nie będziemy mieli problemów z rozbudowywaniem projektu, a ponadto znacząco skrócimy czas kompilacji programu.
Przykład
----------- plik a.h -----------
class A
{
public:
A();
virtual ~A();
int GetSomeValue();
};
----------- plik b.h -----------
#include "a.h" // <- włączamy bo dziedziczymy
class B :public A
{
public:
B();
virtual ~B();
};
----------- plik c.h -----------
#include "d.h"
class A; // <- mamy tylko wskaźnik, nie musimy włączać headera, zrobimy to w .cpp
class C
{
protected:
A * m_pA;
D m_d;
public:
C(A *);
~C();
A* GetA(){ return m_pA; }
//inline int GetAValue(){ return m_pA->GetSomeValue(); } <- tego nie można robić
inline int GetDValue(){ return m_d.GetSomeValue(); }
};
----------- plik d.h -----------
class C;
class D
{
protected:
C * m_pC; // <- klasyczna pętla, ale nie ma żadnych problemów
public:
D();
~D();
int GetSomeValue();
};


