Featured image of post Header guards vs pragma once

Header guards vs pragma once

Abstract

Durante lo sviluppo del progetto “Enne 2D Engine” mi sono posto più volte la domanda: ma perchè continuo a scrivere le header guards qunado posso usare la direttiva #pragma once?

Ho quindi deciso di approfondire la questione fornire la mia analisi.

Perché serve proteggere i file di dichiarazione?

Principio del ODR

Il One Definition Rule è una regola fondamentale da rispettare nel lunguaggio di programmazione C++.

La regola sancisce che una funzione o una variabile debba avere una ed una sola definizione all’interno del singolo programma.

Questo costrutto preveviene i problemi legati alle definizioni multiple della stessa funzione o variabile, che può causare errori e comportamenti indesiderati nel programma.

Violare il principio del ODR può causare problemi come errori di “multiple definitions” in fase di compilazione o addirittura errori a livello del linker, il quale potrebbe non essere in grado di risolvere la definizione di funzione o variabile che deve essere utilizzata.

Nell’esempio sottostante, con i 3 files definiti il processo di compilazione fallisce, a causa di una definizione multipla della struct foo;

File alpha.hpp

1
struct foo {};

File bravo.hpp

1
#include "alpha.hpp"

File charlie.hpp

1
2
#include "alpha.hpp"
#include "bravo.hpp"

Il preprocessore terminate le opportune sostituzioni, restituisce il seguente risultato.

1
2
struct foo {};
struct foo {};

Ma quindi come far sì che il ODR venga rispettato?

La soluzione che, principalmente, salta all’occhio, seppur sia un po’ spartana, è gestire manualmente la gerarchia delle direttive #include. Nell esempio mostrato in precedenza si dovrebbe saltare l’inclusione di alpha.hpp in charlie.hpp. Tuttavia si tratta di una tecnica molto ingenua che non risulta scalabile su soluzioni progettuali medio-grandi.

Esistono due alternative che risolvono il nostro porblema.

  • Header guards.
  • Direttiva #pragma once.

Header guards

La soluzione facente parte dello standard in C++ è l’uso degli header guards. Queste “guardie” o meglio protezioni per le intestazioni (#include <header>), impediscono che un header file venga incluso più di una volta in una singola unità di compilazione. Per ottenere questo risultato utilizzano le macro del preprocessore per verificare se l’intestazione è già stata inclusa in precedenza. Nel caso in cui fosse già stata inclusa, queste clausole impediscono una successiva reinclusione.

Il #define crea una macro, ovvero l’associazione di un identificatore o un identificatore con parametri con una stringa di token. Dopo che la macro è stata definita, il compilatore può sostituire la stringa di token per ogni occorrenza dell’identificatore presente nel file di origine.

Riprendendo l’esempio precedente, con poche modifiche otteniamo:

File alpha.hpp

1
2
3
4
5
6
#ifndef ALPHA_HPP
#define ALPHA_HPP

struct foo {};

#endif // ALPHA_HPP

File bravo.hpp

1
2
3
4
5
6
#ifndef BRAVO_HPP
#define BRAVO_HPP

#include "alpha.hpp"

#endif // BRAVO_HPP

File charlie.hpp

1
2
3
4
5
6
7
#ifndef CHARLIE_HPP
#define CHARLIE_HPP

#include "alpha.hpp"
#include "bravo.hpp"

#endif // CHARLIE_HPP

Il preprocessore terminate le opportune sostituzioni, restituisce il seguente risultato.

1
struct foo {};

Quando si lavora su progetti molto grandi, vi è da tenere ben presente che devono essere definite delle linee guida sulla definizione del nome della macro, altrimenti potrebbero insorgere scontri sui nomi utilizzati. Per questo è sempre utile utilizzare tools come clang-tidy e similari, i quali aiutano anche a far fronte a dimenticanze tipo non chiudere la macro con un #endif. Un punto importante da tenere presente, soprattutto quando si lavora con basi di codice di grandi dimensioni e si usano le protezioni per le intestazioni, è che deve esserci una forte linea guida sul nome della macro utilizzata, poiché l’uso del solo nome del file, ad esempio, può facilmente portare a scontri di nomi. Altri problemi possono sorgere quando si copia e si incolla un file di intestazione e non si riesce a modificare la macro utilizzata, oppure si manca la direttiva #endif.

Io ad esempio per definire il nome di una macro seguo questo schema <PROJECT_ROOT>_<RELATIVE_PATH_TO_HPP_FILE>_<FILE_NAME>_HPP_.

Schema che rende impossibile avere due file con lo stesso identificatore.

Nel dettaglio supponiamo la seguente struttura e la root directory nominata CPP_PROJECT. E ipotizziamo di avere due file con lo stesso nome in due directory differenti.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.
├── libfoo
│   ├── CMakeLists.txt
│   ├── docs
│   │   └── CMakeLists.txt
│   ├── include
│   │   └── libfoo
│   │       ├── detail
│   │       │   └── alpha.hpp
│   │       └── common
│   │           └── alpha.hpp

Il nome della macro sul file alpha.hpp nella directory detail sarà:

1
#define CPP_PROJECT_LIBFOO_INCLUDE_LIBFOO_DETAIL_ALPHA_HPP_

Il nome della macro sul file alpha.hpp nella directory common sarà:

1
#define CPP_PROJECT_LIBFOO_INCLUDE_LIBFOO_COMMON_ALPHA_HPP_

Pragma once

L’alternativa alle header guards, la quale è molto diffusa seppur non parte dello standard di C++, è l’uso della direttiva #pragma once.

File alpha.hpp

1
2
3
#pragma once

struct foo {};

File bravo.hpp

1
2
3
#pragma once

#include "alpha.hpp"

File charlie.hpp

1
2
3
4
#pragma once

#include "alpha.hpp"
#include "bravo.hpp"

Il preprocessore terminate le opportune sostituzioni, restituisce il seguente risultato.

1
struct foo {};

Dunque, si scrive meno codice e si è meno soggetti ad errori riguardanti il clashes sui nomi, ma questa alternativa presenta solo vantaggi?

Non proprio, questa direttiva non è parte dello standard, ergo i compilatori non sono obbligati dallo ISO C++ a fornire il supporto a #pragma once.

Ma perchè non fa parte dello standard?

La risposta è nella complessità che un compilatore affronta per rilevare correttamente e coerentemente l’uguaglianza dei file. Uno dei porblemi più noti è relativo ai symbolic links, riscontrabile sui compilatori GCC e MSVC, porta all’errore di rilevazione dell’uguaglianza dei file includendolo pù di una volta, causando una rottura dello ODR. Si veda https://en.m.wikipedia.org/wiki/Pragma_once#Caveats.

Non vi è, poi, garanzia che il supporto di #pragma once sia lo stesso tra i diversi compilatori, il che può essere un problema per alcuni sviluppatori.

Header guards o pragma once?

Dipende dal caso.

La questione si riduce alla domanda, conviene brattare una compilazione sicura per guadagnare un briciolo di semplicità nella stesura del codice?

Sicuramente no, le header guards sono affidabili e sono riconosciute nello standard del C++, c’è più codice da scrivere, certo, ma almeno non dobbiamo affrontare l’inaffidabilità di #pragma once.

Conclusione

È preferibile l’occasionale errore di compilazione dovuto al clash dei nomi delle macro, piuttosto di aver a che fare con una direttiva di cui non si ha il controllo.

Licensed under CC BY-NC-SA 4.0
Realizzato con Hugo
Tema Stack realizzato da Jimmy