[c++] Unikanie include w plikach naglowkowych

0

Czy powinno sie ograniczac ilosc include w plikach .h? Na pierwszy rzut wydaje mi sie, ze tak bo jezeli mam plik "a.h", ktory zawiera #include "b.h" to wszystkie pliki, ktore potrzebuja "a.h" jednoczesnie dolaczaja "b.h". Ale czy to jest szkodliwe? Jedyne co mi przychodzi na mysl to sytuacja, w ktorej implementacja "a.h" (zalozmy, ze jest tam jakas klasa) jest dolaczana do projektu w postaci skompilowanej. Wtedy nawet jezeli klasa z "a.h" korzysta z plikow naglowkowych, ktorych uzytkownik nie ma, ale tworca umiescil je poza "a.h" to i tak mozna swobodnie korzystac z tej klasy. Czy moze to tez tylko moje urojenia i nie powiniem w ogole sie przejmowac tymi includami?

Problem bylby (dokladnie taki spotkalem) jezeli klasa ma pola, ktorych typy pochodza z b.h np:
b.h:

struct typ
{
	//pola
};

a.h:

#include "b.h"

class CKlasa
{
	//ciach
	typ* pole;
	//ciach
}

W takim przypadku, moglbym zmienic w pliku "a.h" typ pola na void* pole, dolaczyc "b.h" do "a.cpp" (plik z implementacja CKlasa), a potem bawic sie w rzutowanie, kopiowanie itd.... Ale to dodatkowe kombinowanie dla tego najpierw wolalem zapytac czy warto.

Pozdrawiam i z gory dziekuje

0

Witam. Sam pamiętam jeszcze te piękne czasy gdy miałem podobny problem :-).

Sprawa generalnie wygląda tak:

tworząc pliki nagłówkowe, które załączają inne pliki nagłówkowe łatwo jest "zachachmęcić" i

doprowadzić np. do błędu podwójnej definicji typu. Dlatego w każdym pliku stosuje się dodatkowe

instrukcje preprocesora:

#ifndef NAZWA_PLIKU_NAGLOWKOWEGO
#define NAZWA_PLIKU_NAGLOWKOWEGO

//ciało pliku nagłówkowego...

#endif

Taka konstrukcja mówi preprocesorowi "załącz plik nagłówkowy tylko jeśli nie został jeszcze wcześniej

załączony". Zapobiega to podwójej definicji.

A co do tych plików zagnieżdżonych to dzięki powyższym makroinstrukcjom możesz tylko załączyć

główny plik *.h (ten który zawiera wszystkie podrzędne) lub załączać wszystkie po kolei. Tak i tak

będzie działać. (Nie wiem czy wiesz ale np. nagłówek <fstream> automatycznie załącza <iostream>)

1

to się robi tak (na wirtualnym przykładzie):

#ifndef NEWCLASS_H
#define NEWCLASS_H

// forward declarations:
// dla tych klas nie potrzeba dołączać nagłówków bo występują w tym nagłówku JEDYNIE jako REFERENCJE lub WSKAŹNIKI
// w zasadzie wszystko co nie spełnia tej zasady musi mieć dołączony plik nagłówkowy
class SomeOtherClass1;
class SomeOtherClass2;
class SomeOtherClass3;
class SomeOtherClass4;

// poniższe nagłowki muszą wystąpić bo ich klasy występują jako
#include"MyBaseClass.h" //klasa, po której się dziedziczy
#include"MyInterface.h" //klasa, po której się dziedziczy
#include"ExtraClass1.h" //klasa, której obiekt jest przekazywana przez wartość (wartość zwracana)
#include"ExtraClass2.h" //klasa, której obiekt jest przekazywana przez wartość (argument przekazywany przez wartość)
#include"ExtraClass3.h" //klasa, której obiekt jest polem klasy definiowanej w tym nagłówku
#include"ExtraClass4.h" //klasa, na której obiekcie wykonywana jest jakaś operacja w tym nagłówku (użycie w metodzie zdefiniowanej w nagłówku - inline)

class NewClass : public MyBaseClass, 
        public MyInterface
{
public:
       NewClass( SomeOtherClass1& a);
       void metoda1( SomeOtherClass2* b );
       SomeOtherClass3* metoda1( SomeOtherClass2* b );
       
      ExtraClass1 returningByValue();
      ExtraClass2& argumentByValue( ExtraClass2 item );

      void MetodaInlinowa( ExtraClass4* p )
      {  
            p->WykonajDzialanie();
      }
private:
      ExtraClass3 field;
      SomeOtherClass4* pointerToClass;
};
#endif NEWCLASS_H

Chyba ująłem w tym wszystkie przypadki.

0

nie, niestety nie ujales wszystkich.. sam juz kiedys utknalem na probie stworzenia 'szablonu' pliku .h ktory bylby po porstu uniwersalny i poleglem :)

np. - okazuje sie ze czasami trzeba miec tez #include po definicji klas, itd!
najlepsza wersja jaka teraz pamietam miala sekcje:

#ifdef...

#include <bibliotek>

forward decl typow z tego pliku

stale, zmienne globlane, etc

#include "plikow.h-od-ktorych-ten-plik-zalezy-i-koniecznie-WYMAGA-znajomosci-definicji-klas"

forward decl typow od ktorych ten plik zalezy ale NIE wymaga znajomosci definicji

definicje klas tego pliku

#include plikow klas od ktorych ten plik zalezy ale NIE wymaga znajomosci definicji

#endif

wynika to stad, ze jak troche pomyslec -- bardzo nadmiarowe jest #include'owane wszystkich definicji na starcie, nawet jesli jedynie potrzebujemy znac nazwe, bo przekazujemy sobie IKS-* czy IKS-& przez parametry nic z nim faktycznie nie robiac.. szczesliwie rzadko jest to problemem - ja sie natknalem na to przy masie template'ow (cala implementacja w .hpp! wymaga aby .h znal definicje uzywanych klas a nie tylko nazw!) i skomplikowanych powiazaniach dwustronnych pomiedzy klasami (i teraz niech dwa+ template wymagaja na krzyz znania swojej definicji bo jakas tam metoda gleboko chce wywolac metode z template-parama mojego ojca.. "czaisz baze":) )

ogolnie twierdze ze skonstruowanie szablonu .h ktory pokrywalby wszystko jest -- niemozliwe..
chybaaa zeeee korzystamy z rozdzialu templateow na .hpp i .cpp -- ale przy nieobecnosci export to chyba wszyscy wiedza co wprowadza za upierdliwosci..

0

Fakt z template to może być zabawa, bo trudniej obczaić co jest konieczne, a co nie.
Jednak to są bardzo rzadkie przypadki.
Co do ostatniego includa, który proponujesz do "wzorca" nagłówka, czyli:
#include plikow klas od ktorych ten plik zalezy ale NIE wymaga znajomości definicji

Ja zwykle je wstawiam, tylko w cpp, bo te "forward declaration" czasmi powoduje, że w momencie użycia jakiejś metody z niedefiniowanym symbolem, można dostać dziwny błąd kompilacji (który po paru napotkaniach szybko staje się starym znajomym, którego łatwo się poznaje). Często jest to jednak niekonieczne, bo zdarza się, że te metody są w bieżącym module nieużywane (np.: klasa ma rozbudowane API, a w danym module potrzebna jest niewielka jej funkcjonalność).

0

Co do dolaczania wszystkiego w naglowku to jeszcze taka uwaga. Czasami warto w naglowkowym podac deklaracje zapowiadajaca klasy zamiast pliku .h, a ten dodac w pliku .cpp. Dzialanie zwykle jest takie same, ale w przypadku zmian zmniejsza sie (czasem znaczaco) czas rekompilacji projektu :)

0

A no właśnie, ograniczanie jest ważne. Przy krzyżowych robię to co wąż: każdy moduł na 4 części:

  • forwardowe_deklaracje.h
  • deklaracje_klas.h
  • implementacja.hpp // jeśli szablonowe
  • implementacja.cpp // a to już nie jest nagłówek ;)

Większość rzeczy się rozplątuje, postępując wg metody:

  • forward - żadnych include'ów własnych
  • deklaracje - wstawia tylko forwardy
  • implementacja.hpp - wstawia co najwyżej deklaracje
  • implementacja.cpp - wszystko jak leci

Johny zwrócił uwagę nie tylko na to, że krzyżowanie robi problemy. Nagłówki zwiększają czas kompilacji. Dlatego nawet w implementacji.cpp wypadałoby wstawiać tylko tyle, ile trzeba, i ani odrobiny więcej. To akurat robię wg zasady:

  • najpierw wszystko jak leci
  • pod koniec wywalić niepotrzebe moduły
  • potrzebne moduły spróbować zamienić z .hpp na .h (z niektórymi się uda, z innymi nie)
  • z tymi co się udało - może same forwardy wejdą

Dodatkowo, jeśli mam coś, co będzie częścią ogólnej biblioteki, a nie tego-jednego-projektu, to tnę nagłówki nie tylko swoje, ale i cudze. Powody:

  • nie zaśmiecam globalscope (dużo nagłówków jest dla C, nie ma przestrzeni nazw).
  • krócej się kompiluje (niby nie widać, a jednak widać...)
  • własna satysfakcja ;)

Przykładowo:
windows.h to zło - może samo winbase.h starczy. a może tylko stdarg.h mi trzeba?

potrzebuje szerokich znaków wchar_t - no to "stddef.h" idzie... dużo cholera nawsadzał. No to zamiana na:
#define __need_wchar_t
#include <stddef.h>
i taki include nie wstawia do globalscope NIC poza wchar_t ;>

Przykłady patologiczne były. Tak ostro nie trzeba, zwłaszcza, że często zrobimy coś nieprzenośnego. Ale moja zasada życiowa: "robić tyle, ile trzeba, i ani odrobiny więcej" świetnie się ma do include'ów ;)

Sporo powtórzyłem - w innej formie, może podsumowującej, może po prostu w formie mojej-najmojszej. Po prostu wiedz, że myśli które cię nachodzą mają poparcie licznej grupy osób, zwłaszcza, że wśród wypowiadających się są sami doświadczeni jak na razie ;)

0

A i może na zakończenie, że czasami nie warto rozpędzać się z wywalaniem headerów. Niektóre kompilatory wspierają coś takiego jak "precompiled headers".
Sprowadza się to tego, że w w *.cpp wstawia się na samym początku pewną stałą kombinację nagłówków (zwykle nagłówki bibliotek). Kompilator natrafiając na tą kombinację wykorzystuje fragmenty kompilacji z wcześniejszych kompilacji (wyszło masło maślane :) ). W efekcie czasami lepiej wstawić więcej niż potrzeba.

Z moich doświadczeń z Builderem wynika, że warto stworzyć sobie specjalny plik nagłówkowy zawierający te nagłówki i potem do każdego .cpp na samym początku pliku dołączyć ten header i przyspieszenie kompilacji jest przeogromne. W builderze jest odpowiednia dyrektywa informująca, że powinien zakończyć zapamiętywanie precompiled headers.

0

a czemu nikt nie napisał o :P

#pragma once

to działa tak samo jak:

#ifndef NAZWA_PLIKU_NAGLOWKOWEGO
#define NAZWA_PLIKU_NAGLOWKOWEGO

//ciało pliku nagłówkowego...

#endif

tyle, że zautomatyzowanie

0

bo to pragma jest ;P
//quetz: #pragma once z tego co wiem jest oznaczone w coraz wiekszej ilosci kompilatorow jako 'deprecated'

1 użytkowników online, w tym zalogowanych: 0, gości: 1