Czy jest dobrą praktyką umieszczanie definicji klas C++ w pliku nagłówkowym?

Kiedy projektujemy klasy w Javie, Vala lub C# umieszczamy definicję i deklarację w tym samym pliku źródłowym. Jednak w C++ tradycyjnie preferowane jest oddzielenie definicji i deklaracji w dwóch lub więcej plikach.

Co się stanie, jeśli użyję pliku nagłówkowego i wrzucę do niego wszystko, jak Java? Jest kara za występ czy coś?

Author: πάντα ῥεῖ, 2011-02-10

5 answers

Odpowiedź zależy od tego, jaką klasę tworzysz.

Model kompilacji C++pochodzi z czasów C, a więc jego metoda importowania danych z jednego pliku źródłowego do drugiego jest stosunkowo prymitywna. Dyrektywa #include dosłownie kopiuje zawartość dołączanego pliku do pliku źródłowego, a następnie traktuje wynik tak, jakby był to plik, który napisałeś przez cały czas. Musisz być ostrożny z tym powodu polityki C++ o nazwie jedna definicja reguła (ODR), która stwierdza, że każda funkcja i klasa powinny mieć co najwyżej jedną definicję. Oznacza to, że jeśli zadeklarujesz gdzieś klasę, wszystkie funkcje tej klasy powinny być albo nie zdefiniowane w ogóle, albo zdefiniowane dokładnie raz w dokładnie jednym pliku. Są pewne wyjątki (dojdę do nich za chwilę), ale na razie traktuj tę zasadę tak, jakby była to szybka i twarda reguła bez wyjątków.

Jeśli weźmiesz klasę non-template i umieścisz zarówno klasę definicja i implementacja do pliku nagłówkowego mogą pojawić się problemy z regułą one definition. W szczególności, Załóżmy, że mam dwa różne .pliki cpp, które kompiluję, z których oba #include Twój nagłówek zawiera zarówno implementację, jak i interfejs. W tym przypadku, jeśli spróbuję połączyć te dwa pliki razem, linker odkryje, że każdy z nich zawiera kopię kodu implementacji dla funkcji członkowskich klasy. W tym momencie linker zgłosi błąd ponieważ naruszyłeś regułę jednej definicji: istnieją dwie różne implementacje wszystkich funkcji członkowskich klasy.

Aby temu zapobiec, Programiści C++ zazwyczaj dzielą klasy na plik nagłówkowy, który zawiera deklarację klasy, wraz z deklaracjami jej funkcji Członkowskich, bez implementacji tych funkcji. Implementacje są następnie umieszczane w osobnym .plik cpp, który można skompilować i połączyć oddzielnie. Dzięki temu Twój kod może uniknąć wpadłem w kłopoty z ODR. Oto jak. Po pierwsze, za każdym razem, gdy #include plik nagłówkowy klasy jest podzielony na wiele różnych .pliki cpp, każdy z nich dostaje kopię deklaracji funkcji Członkowskich, a nie ich definicji, więc żaden z klientów twojej klasy nie skończy z definicjami. Oznacza to, że dowolna liczba klientów może #include Twój plik nagłówkowy bez problemów podczas łącza. Od czasu twojego .plik cpp z implementacją jest jedynym plik, który zawiera implementacje funkcji Członkowskich, w czasie łącza można połączyć go z dowolną liczbą innych plików obiektów klienta bez kłopotów. To jest główny powód, dla którego dzielisz .h i .pliki cpp osobno.

Oczywiście ODR ma kilka wyjątków. Pierwsza z nich zawiera funkcje i klasy szablonów. ODR wyraźnie stwierdza, że możesz mieć wiele różnych definicji dla tej samej klasy szablonu lub funkcji, pod warunkiem, że wszystkie są odpowiednik. Ma to przede wszystkim ułatwić kompilowanie szablonów - każdy plik C++ może tworzyć instancje tego samego szablonu bez kolizji z innymi plikami. Z tego powodu, a także z kilku innych powodów technicznych, szablony klas zwykle mają po prostu .plik h bez dopasowania .plik cpp. Dowolna liczba klientów może #include pliku bez problemów.

Drugi ważny wyjątek od ODR obejmuje funkcje inline. Spec wyraźnie stwierdza, że ODR nie ma zastosowania do inline funkcje, więc jeśli Masz plik nagłówkowy z implementacją funkcji członka klasy, która jest zaznaczona w linii, to jest to w porządku. Dowolna liczba plików może #include tego pliku bez łamania ODR. Co ciekawe, każda funkcja członka, która jest zadeklarowana i zdefiniowana w ciele klasy, jest niejawnie inline, więc jeśli masz nagłówek taki jak ten:

#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething() {
        /* ... code goes here ... */
    }
};

#endif
Więc nie ryzykujesz złamania ODR. Jeśli przepisz to jako
#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething();
};

void MyClass::DoSomething()  {
    /* ... code goes here ... */
}

#endif

Wtedy ty będziesz łamał ODR, ponieważ funkcja member nie jest zaznaczona w linii, a jeśli jest wiele klientów #include w tym pliku będzie wiele definicji MyClass::DoSomething.

Więc podsumowując - powinieneś chyba podzielić swoje zajęcia na a .h/.para cpp, aby uniknąć złamania ODR. Jednak, jeśli piszesz szablon klasy, nie potrzebujesz .plik cpp (i prawdopodobnie nie powinien mieć go w ogóle), a jeśli nie masz nic przeciwko zaznaczaniu każdej funkcji członka swojej klasy inline, możesz również uniknąć .plik cpp.

 47
Author: templatetypedef,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-10 09:26:04

Wadą umieszczania definicji w plikach nagłówkowych jest: -

Plik nagłówkowy A-zawiera definicję metahodA ()

Plik nagłówka B-zawiera plik nagłówka A.

Teraz powiedzmy, że zmienisz definicję metody. Trzeba by skompilować plik A jak i B z powodu włączenia pliku nagłówka a do B.

 5
Author: sachin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-10 09:19:22

Największa różnica polega na tym, że każda funkcja jest zadeklarowana jako funkcja inline. Ogólnie Twój kompilator będzie na tyle inteligentny, że nie będzie to problemem, ale w najgorszym przypadku spowoduje to regularne błędy strony i sprawi, że kod będzie żenująco powolny. Zwykle kod jest oddzielony ze względów projektowych, a nie ze względu na wydajność.

 2
Author: regality,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-10 09:22:31

Ogólnie rzecz biorąc, dobrą praktyką jest oddzielanie implementacji od nagłówków. Istnieją jednak wyjątki w przypadkach takich jak szablony, w których implementacja trafia do samego nagłówka.

 0
Author: Mahesh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-10 09:15:08

Dwa szczególne problemy z umieszczeniem wszystkiego w nagłówku:

  1. Czas kompilacji zostanie zwiększony, czasami znacznie. Czasy kompilacji C++ są na tyle długie, że nie jest to coś, czego chcesz.

  2. Jeśli masz okrągłe zależności w implementacji, utrzymanie wszystkiego w nagłówkach jest trudne do niemożliwego. eg:

    Nagłówek 1.h

    struct C1
    {
      void f();
      void g();
    };
    

    Nagłówek 2.h

    struct C2
    {
      void f();
      void g();
    };
    

    Impl1.cpp

    #include "header1.h"
    #include "header2.h"
    
    void C1::f()
    {
      C2 c2;
      c2.f();
    }
    

    Impl2.cpp

    #include "header2.h"
    #include "header1.h"
    
    void C2::g()
    {
      C1 c1;
      c1.g();
    }
    
 0
Author: ymett,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-10 10:32:35