style guide google code c++ coding-style

c++ - guide - google code style java



#pragma una vez vs incluir guardias? (13)

Estoy trabajando en una base de código que se sabe que solo se ejecuta en Windows y se compila en Visual Studio (se integra estrechamente con Excel, por lo que no se va a ninguna parte). Me pregunto si debería ir con los guardias tradicionales incluidos o usar #pragma once para nuestro código. Creo que dejar que el compilador se ocupe de #pragma once producirá compilaciones más rápidas y es menos propenso a errores al copiar y pegar. También es un poco menos feo ;)

Nota: para obtener los tiempos de compilación más rápidos, podríamos utilizar Guardias de inclusión redundantes, pero eso agrega un acoplamiento estrecho entre el archivo incluido y el archivo incluido. Por lo general, está bien porque la guarda debería basarse en el nombre del archivo y solo cambiaría si fuera necesario cambiar el nombre de inclusión de todos modos.


Desde la perspectiva de un probador de software

#pragma once es más corto que un protector incluido, menos propenso a errores, soportado por la mayoría de los compiladores, y algunos dicen que compila más rápido (lo que ya no es cierto [ya]).

Pero todavía te sugiero que vayas con #ifndef estándar incluye guardias.

¿Por qué #ifndef ?

Considere una jerarquía de clases inventada como esta donde cada una de las clases A , B y C viven dentro de su propio archivo:

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

Ahora supongamos que estás escribiendo pruebas para tus clases y necesitas simular el comportamiento de la clase B realmente compleja. Una forma de hacerlo sería escribir una clase simulada usando, por ejemplo, Google Mock y colocarla dentro de un directorio mock mocks/bh . Tenga en cuenta que el nombre de la clase no ha cambiado, pero solo se almacena dentro de un directorio diferente. Pero lo más importante es que el protector de inclusión se nombra exactamente igual que en el archivo original bh .

mocks / 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

¿Cuál es el beneficio?

Con este enfoque, puede burlarse del comportamiento de la clase B sin tocar la clase original o decirle a C sobre ello. Todo lo que tiene que hacer es colocar el directorio mocks/ en la ruta de inclusión de su complaciente.

¿Por qué no se puede hacer esto con #pragma once ?

Si hubiera usado #pragma once , obtendría un choque de nombres porque no puede protegerlo de definir la clase B dos veces, una vez que la original y una vez la versión simulada.


#pragma once tiene errores que no se pueden arreglar . Nunca se debe utilizar.

Si su ruta de búsqueda #include es lo suficientemente complicada, es posible que el compilador no pueda distinguir la diferencia entre dos encabezados con el mismo nombre base (por ejemplo, a/foo.h b/foo.h ), por lo que un #pragma once en uno de ellos. suprimirá ambos . También puede ser incapaz de decir que dos parientes diferentes incluyen (por ejemplo, #include "foo.h" y #include "../a/foo.h" refieren al mismo archivo, por lo que #pragma once no podrá suprimir un redundante Incluir cuando debería haberlo hecho.

Esto también afecta la capacidad del compilador para evitar releer archivos con guardias #ifndef , pero eso es solo una optimización. Con #ifndef guardias, el compilador puede leer con seguridad cualquier archivo que no esté seguro de haber visto ya; Si está mal, solo tiene que hacer un trabajo extra. Siempre que no haya dos encabezados que definan la misma macro de guarda, el código se compilará como se esperaba. Y si dos encabezados definen la misma macro de guarda, el programador puede entrar y cambiar una de ellas.

#pragma once no tiene dicha red de seguridad: si el compilador está equivocado acerca de la identidad de un archivo de encabezado, de cualquier forma , el programa no se compilará. Si golpea este error, sus únicas opciones son dejar de usar #pragma once , o cambiar el nombre de uno de los encabezados. Los nombres de los encabezados son parte de su contrato de API, por lo que cambiar el nombre probablemente no sea una opción.

(La versión corta de por qué esto no se puede arreglar es que ni la API del sistema de archivos de Unix ni la de Windows ofrecen ningún mecanismo que garantice decirle si dos rutas de acceso absolutas se refieren al mismo archivo. Si tiene la impresión de que los números de inodo se pueden usar para que, lo siento, te equivocas

(Nota histórica: la única razón por la que no eliminé #pragma once y #import fuera de GCC cuando tenía la autoridad para hacerlo, hace unos 12 años, los encabezados del sistema de Apple confiaban en ellos. En retrospectiva, eso no debería me han detenido.)

(Ya que esto se ha presentado dos veces en el hilo de comentarios: los desarrolladores de GCC hicieron un gran esfuerzo para que #pragma once más confiable posible; consulte el informe de errores de GCC 11569. Sin embargo, la implementación en las versiones actuales de GCC todavía puede Fallo bajo condiciones plausibles, como construir granjas que sufren distorsión del reloj. No sé cómo será la implementación de ningún otro compilador, pero no espero que nadie lo haya hecho mejor .


Creo que lo primero que debes hacer es verificar si esto realmente va a hacer una diferencia, es decir. Primero debes probar el rendimiento. Una de las búsquedas en google arrojó this .

En la página de resultados, las columnas están ligeramente apagadas para mí, pero está claro que al menos hasta VC6 Microsoft no estaba implementando las optimizaciones de protección de inclusión que usaban las otras herramientas. Cuando el protector de inclusión era interno, tomó 50 veces más tiempo que el protector de inclusión externo (los guardias de inclusión externos son al menos tan buenos como #pragma). Pero consideremos el posible efecto de esto:

De acuerdo con las tablas presentadas, el tiempo para abrir la inclusión y la verificación es 50 veces mayor que el de un #pragma equivalente. ¡Pero el tiempo real para hacerlo se midió en 1 microsegundo por archivo en 1999!

Entonces, ¿cuántos encabezados duplicados tendrá un solo TU? Esto depende de su estilo, pero si decimos que una TU promedio tiene 100 duplicados, en 1999 estamos pagando potencialmente 100 microsegundos por TU. Con las mejoras de HDD, esto es probablemente mucho más bajo ahora, pero incluso con encabezados precompilados y un correcto seguimiento de las dependencias, el costo total acumulado de esto para un proyecto es casi seguramente una parte poco importante de su tiempo de construcción.

Ahora, por otro lado, por muy improbable que sea, si alguna vez te mueves a un compilador que no admite #pragma once , considera cuánto tiempo tomará actualizar tu base de origen completa para incluir guardas en lugar de # pragma

No hay razón para que Microsoft no pueda implementar una optimización de inclusión de guardado de la misma manera que GCC y todos los demás compiladores (en realidad, ¿alguien puede confirmar si sus versiones más recientes implementan esto?). En mi humilde opinión, #pragma once hace muy poco más que limitar su elección de compilador alternativo.


Después de participar en una discusión extensa sobre el supuesto intercambio de desempeño entre #pragma once y #ifndef guardias contra el argumento de la corrección o no (estaba tomando el lado de #pragma once basado en un adoctrinamiento relativamente reciente para ese fin), decidí para finalmente probar la teoría de que #pragma once es más rápido porque el compilador no tiene que intentar volver a incluir #include un archivo que ya se había incluido.

Para la prueba, generé automáticamente 500 archivos de encabezado con interdependencias complejas, y tuve un archivo .c que no #include todos ellos. Corrí la prueba de tres maneras, una vez con solo #ifndef , una vez con solo #pragma once , y una vez con ambas. Realicé la prueba en un sistema bastante moderno (un MacBook Pro 2014 con OSX, utilizando el Clang incluido de XCode, con el SSD interno).

Primero, el código de prueba:

#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; }

Y ahora, mis varias pruebas de ejecución:

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

Como puede ver, las versiones con #pragma once fueron de hecho un poco más rápidas de preproceso que las de #ifndef , pero la diferencia fue bastante insignificante, y quedaría muy opacada por la cantidad de tiempo que la construcción y la vinculación del código tomar. Tal vez con una base de código lo suficientemente grande, en realidad podría llevar a una diferencia en los tiempos de compilación de unos pocos segundos, pero entre los compiladores modernos poder optimizar #ifndef guardias #ifndef , el hecho de que los sistemas operativos tienen buenos cachés de discos y las crecientes velocidades de la tecnología de almacenamiento, Parece que el argumento de rendimiento es discutible, al menos en un sistema de desarrollador típico en esta época. Los entornos de compilación más antiguos y más exóticos (por ejemplo, encabezados alojados en una red compartida, compilación a partir de cintas, etc.) pueden cambiar un poco la ecuación, pero en esas circunstancias parece más útil simplemente crear un entorno de compilación menos frágil en primer lugar.

El hecho del asunto es que #ifndef está estandarizado con el comportamiento estándar, mientras que #pragma once no lo es, y #ifndef también maneja casos de esquinas de rutas de búsqueda y sistemas de archivos extraños, mientras que #pragma once puede confundirse por ciertas cosas, lo que lleva a un comportamiento incorrecto que el programador no tiene control sobre El principal problema con #ifndef es que los programadores eligen nombres #ifndef para sus guardias (con colisiones de nombres, etc.) y aún así es bastante posible que el consumidor de una API anule esos nombres pobres usando #undef . Quizás no sea la solución perfecta. pero es posible , mientras que #pragma once no tiene recurso si el compilador está seleccionando erróneamente un #include .

Por lo tanto, aunque #pragma once sea ​​demostrablemente (un poco) más rápido, no estoy de acuerdo en que esto en sí mismo sea una razón para usarlo sobre guardias #ifndef .

EDITAR : Gracias a los comentarios de @LightnessRacesInOrbit, he aumentado el número de archivos de encabezado y he cambiado la prueba para ejecutar solo el paso del preprocesador, eliminando la pequeña cantidad de tiempo que se estaba agregando en el proceso de compilación y enlace (que era trivial antes y inexistente ahora). Como era de esperar, el diferencial es aproximadamente el mismo.


Generalmente no me molesto con #pragma once ya que mi código a veces tiene que compilar con algo que no sea MSVC o GCC (los compiladores para sistemas integrados no siempre tienen el #pragma).

Así que tengo que usar guardias #include de todos modos. También podría usar #pragma once como sugieren algunas respuestas, pero no parece haber mucha razón y con frecuencia causará advertencias innecesarias en los compiladores que no lo admiten.

No estoy seguro de qué ahorro de tiempo podría traer el pragma. He escuchado que los compiladores generalmente reconocen cuando un encabezado no tiene nada más que comentarios fuera de las macros de guarda y hará el #pragma once equivalente en ese caso (es decir, nunca vuelva a procesar el archivo). Pero no estoy seguro de si es verdad o solo un caso de compiladores podría hacer esta optimización.

En cualquier caso, es más fácil para mí usar guardias #include que funcionarán en todas partes y no se preocuparán más por ello.


Hasta que el día #pragma once convierta en estándar (eso no es actualmente una prioridad para los estándares futuros), sugiero que lo use Y use guardias, de esta manera:

#ifndef BLAH_H #define BLAH_H #pragma once // ... #endif

Las razones son:

  • #pragma once no es estándar, por lo que es posible que algún compilador no proporcione la funcionalidad. Dicho esto, todos los compiladores principales lo soportan. Si un compilador no lo sabe, al menos será ignorado.
  • Como no hay un comportamiento estándar para #pragma once , no debe asumir que el comportamiento será el mismo en todos los compiladores. Los guardias se asegurarán al menos que la suposición básica sea la misma para todos los compiladores que al menos implementen las instrucciones necesarias del preprocesador para los guardias.
  • En la mayoría de los compiladores, #pragma once acelerará la compilación (de un cpp) porque el compilador no volverá a abrir el archivo que contiene esta instrucción. Así que tenerlo en un archivo puede ayudar, o no, dependiendo del compilador. Escuché que g ++ puede hacer la misma optimización cuando se detectan guardas, pero hay que confirmarlas.

Usando los dos juntos obtienes lo mejor de cada compilador para esto.

Ahora, si no tiene un script automático para generar los guardias, podría ser más conveniente usar #pragma once . Sólo sé lo que eso significa para el código portátil. (Estoy usando VAssistX para generar los guardias y pragma una vez rápidamente)

Casi siempre debe pensar su código de manera portátil (porque no sabe de qué está hecho el futuro), pero si realmente piensa que no debe compilarse con otro compilador (código para hardware incorporado muy específico, por ejemplo) luego, solo debes consultar la documentación del compilador sobre #pragma once para saber lo que realmente estás haciendo.


Hay una pregunta relacionada a la que respondí :

#pragma once tiene un inconveniente (aparte de no ser estándar) y es que si tiene el mismo archivo en diferentes ubicaciones (tenemos esto porque nuestro sistema de compilación copia los archivos), el compilador pensará que estos son archivos diferentes.

También estoy agregando la respuesta aquí en caso de que alguien tropiece con esta pregunta y no con la otra.


No creo que suponga una diferencia significativa en el tiempo de compilación, pero #pragma once está muy bien soportado en los compiladores, pero en realidad no es parte del estándar. El preprocesador puede ser un poco más rápido con él, ya que es más sencillo comprender su intención exacta.

#pragma once es menos propenso a cometer errores y es menos código para escribir.

Para acelerar el tiempo de compilación, simplemente reenvíe la declaración en lugar de incluirla en los archivos .h cuando pueda.

Prefiero usar #pragma once .

Vea este artículo de wikipedia sobre la posibilidad de usar ambos .


Para aquellos que deseen usar #pragma una vez e incluir guardias juntos: si no está usando MSVC, entonces no obtendrá una gran optimización de #pragma una vez.

Y no debe poner "#pragma once" en un encabezado que se supone que se incluirá varias veces con cada inclusión posiblemente teniendo un efecto diferente.

Here hay una discusión detallada con ejemplos sobre el uso de #pragma once.


Si está seguro de que nunca usará este código en un compilador que no lo admita (Windows / VS, GCC y Clang son ejemplos de compiladores que sí lo admiten), entonces puede usar #pragma una vez sin preocupaciones. .

También puede usar ambos (ver el ejemplo a continuación), de modo que obtenga portabilidad y aceleración de compilación en sistemas compatibles

#pragma once #ifndef _HEADER_H_ #define _HEADER_H_ ... #endif


Solo quería agregar a esta discusión que estoy compilando en VS y GCC, y solía usar guardias de inclusión. Ahora he cambiado a #pragma once , y la única razón para mí no es el rendimiento, la portabilidad o el estándar, ya que no me importa lo que es estándar siempre que VS y GCC lo admitan, y eso es lo siguiente:

#pragma once reduce las posibilidades de errores.

Es muy fácil copiar y pegar un archivo de encabezado a otro archivo de encabezado, modificarlo para que se ajuste a las necesidades de uno, y olvidarse de cambiar el nombre del protector de inclusión. Una vez que se incluyen ambos, le lleva un tiempo rastrear el error, ya que los mensajes de error no son necesariamente claros.


#pragma once permite al compilador omitir el archivo completamente cuando vuelve a ocurrir, en lugar de analizar el archivo hasta que llega a los guardias #include.

Como tales, la semántica es un poco diferente, pero son idénticas si se usan de la manera en que están destinadas a ser utilizadas.

Combinar ambos es probablemente la ruta más segura, como en el peor de los casos (un compilador que marca pragmas desconocidos como errores reales, no solo advertencias), solo tendría que eliminar los # pragma''s.

Cuando limite sus plataformas a, diga "compiladores convencionales en el escritorio", podría omitir de manera segura los guardias #include, pero también me siento incómodo con eso.

OT: si tienes otros consejos / experiencias para compartir sobre la aceleración de las construcciones, sería curioso.


Encima de la explicación de arriba.

Un breve resumen:

  • Cuando lo usamos # pragma oncees gran parte de la responsabilidad del compilador, no permitir su inclusión más de una vez. Lo que significa que, después de mencionar el fragmento de código en el archivo, ya no es su responsabilidad.

Ahora, el compilador busca, para este fragmento de código al principio del archivo, y evita que se incluya (si ya se incluyó una vez). Esto definitivamente reducirá el tiempo de compilación (en promedio y en un sistema enorme). Sin embargo, en caso de simulaciones / entorno de prueba, dificultará la implementación de los casos de prueba, debido a las dependencias circulares, etc.

  • Ahora, cuando usamos el #ifndef XYZ_Hpara los encabezados, es más responsabilidad de los desarrolladores mantener la dependencia de los encabezados. Lo que significa que, siempre que se deba a un nuevo archivo de encabezado, existe la posibilidad de la dependencia circular, el compilador simplemente marcará algunos " undefined .." mensajes de error en el momento de la compilación, y es usuario verificar la conexión / flujo lógico de las entidades y corregir el problema. incluye.

Esto definitivamente se agregará al tiempo de compilación (según sea necesario rectificar y volver a ejecutar). Además, como funciona sobre la base de incluir el archivo, basado en el estado definido "XYZ_H", y aún se queja, si no puede obtener todas las definiciones.

Por lo tanto, para evitar situaciones como esta, debemos usar, como;

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

Es decir, la combinación de ambos.