c++ - style - #pragma, una volta, includi le guardie?




google style (9)

Dal punto di vista del tester del software

#pragma once è più breve di una guardia di inclusione, meno incline agli errori, supportata dalla maggior parte dei compilatori e alcuni dicono che compila più velocemente (il che non è vero [più lungo]).

Ma ti suggerisco ancora di andare con le guardie standard di #ifndef .

Perché #ifndef ?

Considera una gerarchia di classi artificiosa come questa in cui ciascuna delle classi A , B e C vive all'interno del proprio file:

ah

#ifndef A_H
#define A_H

class A {
public:
  // some virtual functions
};

#endif

bh

#ifndef B_H
#define B_H

#include "a.h"

class B : public A {
public:
  // some functions
};

#endif

ch

#ifndef C_H
#define C_H

#include "b.h"

class C : public B {
public:
  // some functions
};

#endif

Supponiamo ora che tu stia scrivendo dei test per le tue classi e devi simulare il comportamento della classe B veramente complessa. Un modo per farlo sarebbe quello di scrivere una classe di simulazione usando ad esempio google mock e metterlo in una directory mocks/bh . Nota che il nome della classe non è cambiato ma è solo memorizzato in una directory diversa. Ma la cosa più importante è che la guardia include è chiamata esattamente come nel file originale bh .

schernisce / bh

#ifndef B_H
#define B_H

#include "a.h"
#include "gmock/gmock.h"

class B : public A {
public:
  // some mocks functions
  MOCK_METHOD0(SomeMethod, void());
};

#endif

Qual è il vantaggio?

Con questo approccio puoi prendere in giro il comportamento della classe B senza toccare la classe originale o dirlo a C Tutto quello che devi fare è mettere i gestori delle directory mocks/ nel percorso di inclusione del tuo compilatore.

Perché non può essere fatto con #pragma once ?

Se avessi usato #pragma once , avresti ottenuto un conflitto di nomi perché non può proteggerti dal definire la classe B due volte, una volta quella originale e una volta la versione derisa.

Sto lavorando su un codebase che è noto per funzionare solo su Windows ed essere compilato in Visual Studio (si integra strettamente con Excel, quindi non sta andando da nessuna parte). Mi chiedo se dovrei andare con le tradizionali guardie incluse o usare #pragma once per il nostro codice. Penserei che lasciare che il compilatore abbia a che fare con #pragma once produrrà compilazioni più veloci ed è meno soggetto a errori quando si copiano e incollano. È anche leggermente meno brutto ;)

Nota: per ottenere tempi di compilazione più rapidi, è possibile utilizzare le protezioni Incluse ridondanti, ma ciò aggiunge un accoppiamento stretto tra il file incluso e il file incluso. Di solito è ok perché la guardia dovrebbe essere basata sul nome del file e cambierebbe solo se fosse necessario cambiare comunque il nome di include.


#pragma once ha bug non risolvibili . Non dovrebbe mai essere usato.

Se il tuo #include percorso di ricerca è sufficientemente complicato, il compilatore potrebbe non essere in grado di distinguere tra due intestazioni con lo stesso nome di base (ad esempio a/foo.h e b/foo.h ), quindi un #pragma once in uno di essi sopprimerà entrambi . Potrebbe anche non essere in grado di dire che due diversi parenti includono (ad esempio #include "foo.h" e #include "../a/foo.h" riferiscono allo stesso file, quindi #pragma once non riuscirà a sopprimere un ridondante includere quando dovrebbe.

Ciò influisce anche sulla capacità del compilatore di evitare di rileggere i file con le guardie #ifndef , ma questa è solo un'ottimizzazione. Con le guardie #ifndef , il compilatore può tranquillamente leggere qualsiasi file che non è sicuro di aver già visto; se è sbagliato, deve solo fare un lavoro extra. Finché non ci sono due intestazioni che definiscono la stessa macro di guardia, il codice verrà compilato come previsto. E se due intestazioni definiscono la stessa macro di guardia, il programmatore può entrare e cambiarne una.

#pragma once non ha una tale rete di sicurezza - se il compilatore ha torto sull'identità di un file di intestazione, in entrambi i casi , il programma non riuscirà a compilare. Se riscontri questo errore, le tue uniche opzioni sono di smettere di usare #pragma once o di rinominare una delle intestazioni. I nomi delle intestazioni fanno parte del tuo contratto API, quindi la rinomina non è probabilmente un'opzione.

(La versione breve del motivo per cui ciò non è risolvibile è che né l'API Unix né l'API del filesystem Windows offrono alcun meccanismo che garantisca di dire se due percorsi assoluti si riferiscono allo stesso file. Se hai l'impressione che i numeri inode possano essere usati per quello, scusa, ti sbagli.)

(Nota storica: l'unica ragione per cui non ho strappato #pragma once e #import da GCC quando ho avuto l'autorità di farlo, ~ 12 anni fa, le intestazioni di sistema di Apple si basavano su di esse. In retrospettiva, questo non dovrebbe mi hanno fermato.)

(Dal momento che questo è apparso due volte nella discussione dei commenti: gli sviluppatori di GCC hanno fatto un bel po 'di sforzi per rendere #pragma once più affidabile possibile, vedi il bug report GCC 11569. Tuttavia, l'implementazione nelle attuali versioni di GCC può ancora fallire in condizioni plausibili, come le fattorie di costruzione che soffrono di disallineamento dell'orologio. Non so come sia l'implementazione di qualsiasi altro compilatore, ma non mi aspetto che nessuno abbia fatto di meglio .)


Dopo aver intrapreso una discussione estesa sul presunto compromesso di prestazioni tra le guardie di #pragma once e #ifndef contro l'argomento della correttezza o meno (stavo prendendo il lato di #pragma once basato su un indottrinamento relativamente recente a tale scopo), ho deciso per testare finalmente la teoria che #pragma once più veloce perché il compilatore non deve cercare di #include un file che è già stato incluso.

Per il test, ho generato automaticamente 500 file di intestazione con interdipendenze complesse e disponevo di un file .c che #include tutti. Ho eseguito il test in tre modi, una volta con #ifndef , una volta con #pragma once volta e una volta con entrambi. Ho eseguito il test su un sistema abbastanza moderno (un MacBook Pro 2014 con OSX in esecuzione, utilizzando Clan in dotazione di XCode, con l'SSD interno).

Innanzitutto, il codice di prova:

#include <stdio.h>

//#define IFNDEF_GUARD
//#define PRAGMA_ONCE

int main(void)
{
    int i, j;
    FILE* fp;

    for (i = 0; i < 500; i++) {
        char fname[100];

        snprintf(fname, 100, "include%d.h", i);
        fp = fopen(fname, "w");

#ifdef IFNDEF_GUARD
        fprintf(fp, "#ifndef _INCLUDE%d_H\n#define _INCLUDE%d_H\n", i, i);
#endif
#ifdef PRAGMA_ONCE
        fprintf(fp, "#pragma once\n");
#endif


        for (j = 0; j < i; j++) {
            fprintf(fp, "#include \"include%d.h\"\n", j);
        }

        fprintf(fp, "int foo%d(void) { return %d; }\n", i, i);

#ifdef IFNDEF_GUARD
        fprintf(fp, "#endif\n");
#endif

        fclose(fp);
    }

    fp = fopen("main.c", "w");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "#include \"include%d.h\"\n", i);
    }
    fprintf(fp, "int main(void){int n;");
    for (int i = 0; i < 100; i++) {
        fprintf(fp, "n += foo%d();\n", i);
    }
    fprintf(fp, "return n;}");
    fclose(fp);
    return 0;
}

E ora, i miei vari test funzionano:

folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.164s
user    0m0.105s
sys 0m0.041s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.140s
user    0m0.097s
sys 0m0.018s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.193s
user    0m0.143s
sys 0m0.024s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.031s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.170s
user    0m0.109s
sys 0m0.033s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.155s
user    0m0.105s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out 
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.153s
user    0m0.101s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.181s
user    0m0.133s
sys 0m0.020s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c  > /dev/null

real    0m0.167s
user    0m0.119s
sys 0m0.021s
folio[~/Desktop/pragma] fluffy$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/c++/4.2.1
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin17.0.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Come puoi vedere, le versioni con #pragma once erano effettivamente leggermente più veloci rispetto al #ifndef -solo, ma la differenza era del tutto trascurabile e sarebbe stata oscurata dalla quantità di tempo che la creazione e il collegamento del codice avrebbero effettivamente prendere. Forse con una base di codice abbastanza grande potrebbe effettivamente portare a una differenza nei tempi di compilazione di pochi secondi, ma tra i moderni compilatori che sono in grado di ottimizzare le guardie #ifndef , il fatto che i sistemi operativi abbiano una buona cache del disco e le crescenti velocità della tecnologia di archiviazione, sembra che l'argomento delle prestazioni sia discutibile, almeno su un tipico sistema di sviluppo in questo giorno ed età. Ambienti di build più vecchi e più esotici (ad esempio intestazioni ospitate su una condivisione di rete, creazione di nastri, ecc.) Possono modificare l'equazione, ma in tali circostanze sembra più utile semplicemente creare un ambiente di compilazione meno fragile in primo luogo.

#ifndef è standardizzato con un comportamento standard mentre #pragma once non lo è, e #ifndef gestisce anche casi strani di casi di file system e di percorsi di ricerca mentre #pragma once può diventare molto confuso da certe cose, portando a comportamenti scorretti che il programmatore non ha controllo. Il problema principale con #ifndef sono i programmatori che scelgono i brutti nomi per le loro guardie (con collisioni di nomi e così via) e anche allora è abbastanza probabile che l'utente di un'API ignori quei nomi poveri usando #undef - non una soluzione perfetta, forse, ma è possibile , mentre #pragma once non ha alcun ricorso se il compilatore sbaglia erroneamente un #include .

Quindi, anche se #pragma once è dimostrabile (leggermente) più veloce, non sono d'accordo che ciò sia di per sé un motivo per usarlo sulle guardie di #ifndef .

EDIT : Grazie al feedback di @LightnessRacesInOrbit ho aumentato il numero di file di intestazione e modificato il test per eseguire solo la fase del preprocessore, eliminando qualsiasi piccola quantità di tempo veniva aggiunta dal processo di compilazione e collegamento (che era banale prima e inesistente ora). Come previsto, il differenziale è circa lo stesso.


Fino al giorno in cui #pragma once diventa standard (non è attualmente una priorità per gli standard futuri), ti suggerisco di usarlo E usare le guardie, in questo modo:

#ifndef BLAH_H
#define BLAH_H
#pragma once

// ...

#endif

Le ragioni sono:

  • #pragma once non è standard, quindi è possibile che alcuni compilatori non forniscano la funzionalità. Detto questo, tutti i principali compilatori lo supportano. Se un compilatore non lo sa, almeno sarà ignorato.
  • Poiché non esiste un comportamento standard per #pragma once , non si deve assumere che il comportamento sarà lo stesso su tutto il compilatore. Le guardie assicureranno almeno che l'assunto di base sia lo stesso per tutti i compilatori che implementano almeno le istruzioni del preprocessore necessarie per le guardie.
  • Sulla maggior parte dei compilatori, #pragma once velocizzerà la compilazione (di un cpp) perché il compilatore non riaprirà il file contenente questa istruzione. Quindi averlo in un file potrebbe essere di aiuto, o meno, a seconda del compilatore. Ho sentito che g ++ può fare la stessa ottimizzazione quando vengono rilevate protezioni, ma deve essere confermata.

Usando i due insieme ottieni il meglio da ogni compilatore per questo.

Ora, se non hai uno script automatico per generare le guardie, potrebbe essere più comodo usare #pragma once . Basta sapere cosa significa per codice portatile. (Sto usando VAssistX per generare le guardie e il pragma una volta in fretta)

Dovresti quasi sempre pensare il tuo codice in modo portabile (perché non sai di cosa sia fatto il futuro) ma se pensi davvero che non è pensato per essere compilato con un altro compilatore (ad esempio codice per hardware embedded molto specifico) quindi dovresti semplicemente controllare la documentazione del compilatore su #pragma once per sapere cosa stai facendo veramente.


Non penso che farà una differenza significativa in fase di compilazione, ma #pragma once è molto ben supportato tra i compilatori ma non in realtà parte dello standard. Il preprocessore può essere un po 'più veloce con esso in quanto è più semplice capire il tuo intento preciso.

#pragma once è meno incline a fare errori ed è meno codice da digitare.

Per accelerare il tempo di compilazione, è sufficiente inoltrare la dichiarazione anziché includere i file .h quando possibile.

Preferisco usare #pragma once .

Vedi questo articolo su Wikipedia su la possibilità di utilizzare entrambi .


Penso che la prima cosa che dovresti fare sia controllare se questo farà davvero la differenza, ad es. dovresti prima testare la performance. Una delle ricerche in google ha gettato this .

Nella pagina dei risultati, le colonne sono leggermente al di fuori per me, ma è chiaro che almeno fino a VC6 microsoft non implementava le ottimizzazioni di include guard che gli altri strumenti stavano usando. Dove la guardia inclusa era interna, ci sono voluti 50 volte più a lungo rispetto a dove la guardia include era esterna (le guardie esterne incluse sono almeno buone come #pragma). Ma consideriamo il possibile effetto di questo:

Secondo le tabelle presentate, il tempo di aprire l'inclusione e di controllarlo è 50 volte quello di un equivalente #pragma. Ma il tempo reale per farlo è stato misurato a 1 microsecondo per file nel 1999!

Quindi, quante intestazioni duplicate avrà una TU singola? Questo dipende dal tuo stile, ma se diciamo che una TU media ha 100 duplicati, nel 1999 pagheremo potenzialmente 100 microsecondi per TU. Con i miglioramenti dell'HDD, questo è probabilmente molto più basso ormai, ma anche in questo caso con intestazioni precompilate e tracciamento della dipendenza corretto il costo totale cumulativo di questo per un progetto è quasi certamente una parte insignificante del tempo di costruzione.

Ora, il rovescio della medaglia, per quanto improbabile, se mai si passa a un compilatore che non supporta #pragma once quindi considera quanto tempo ci vorrà per aggiornare l'intera base di origine di avere guardie piuttosto che # pragma?

Non c'è motivo per cui Microsoft non possa implementare l'ottimizzazione di include guard allo stesso modo in cui GCC e ogni altro compilatore (in realtà qualcuno può confermare se le loro versioni più recenti lo implementano?). IMHO, #pragma once fa ben poco oltre a limitare la scelta del compilatore alternativo.


Se sei sicuro che non userai mai questo codice in un compilatore che non lo supporta (Windows / VS, GCC e Clang sono esempi di compilatori che lo supportano), allora puoi sicuramente usare #pragma una volta senza preoccupazioni .

Puoi anche usare entrambi (vedi esempio sotto), in modo da ottenere portabilità e accelerazione della compilazione su sistemi compatibili

#pragma once
#ifndef _HEADER_H_
#define _HEADER_H_

...

#endif

Volevo solo aggiungere a questa discussione che sto solo compilando su VS e GCC, e usavo includere le guardie. Ora sono passato a #pragma once , e l'unica ragione per me non sono le prestazioni o la portabilità o lo standard in quanto non mi interessa quello che è standard finché VS e GCC lo supportano, e cioè che:

#pragma once riduce le possibilità di errori.

È fin troppo facile copiare e incollare un file di intestazione in un altro file di intestazione, modificarlo per soddisfare le proprie esigenze e dimenticare di cambiare il nome della guardia di inclusione. Una volta che entrambi sono inclusi, ci vuole un po 'per rintracciare l'errore, in quanto i messaggi di errore non sono necessariamente chiari.


Atop explanation by above.

A brief summary:

  • when we use # pragma once it is much of the compiler responsibility, not to allow its inclusion more than once. Which means, after you mention the code-snippet in the file, it is no more your responsibility.

Now, compiler looks, for this code-snippet at the beginning of the file, and skips it from being included (if already included once). This definitely will reduce the compilation-time (on an average and in huge-system). However, in case of mocks/test environment, will make the test-cases implementation difficult, due to circular etc dependencies.

  • Now, when we use the #ifndef XYZ_H for the headers, it is more of the developers responsibility to maintain the dependency of headers. Which means, whenever due to some new header file, there is possibility of the circular dependency, compiler will just flag some " undefined .. " error messages at compile time, and it is user to check the logical connection/flow of the entities and rectify the improper includes.

Questo sicuramente aggiungerà al tempo di compilazione (come necessità di rettificare e rieseguire). Inoltre, poiché funziona sulla base dell'inserimento del file, basato sullo stato definito "XYZ_H", e continua a lamentarsi, se non è in grado di ottenere tutte le definizioni.

Pertanto, per evitare situazioni come questa, dovremmo usare, come;

#pragma once
#ifndef XYZ_H
#define XYZ_H
...
#endif

cioè la combinazione di entrambi.





coding-style