pl en

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;
  • 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();
        };
        
        
Strona główna: akolacz.tarchomin.pl
css xhtml