¿Por qué no se asume #pragma una vez automáticamente?

81

¿Cuál es el punto de decirle al compilador específicamente que incluya el archivo solo una vez? ¿No tendría sentido por defecto? ¿Existe alguna razón para incluir un solo archivo varias veces? ¿Por qué no simplemente asumirlo? ¿Tiene que ver con hardware específico?

Johnny Cache
fuente
24
¿Existe alguna razón para incluir un solo archivo varias veces? => Podría ser. Un archivo puede tener compilación condicional #ifdefs. Entonces podría decir #define MODE_ONE 1y luego #include "has-modes.h", y luego #undef MODE_ONEcon #define MODE_TWO 1y #include "has-modes.h"otra vez. El preprocesador es agnóstico sobre este tipo de cosas, y tal vez a veces pueden tener sentido.
HostileFork dice que no confíes en SE
66
Tendría sentido como predeterminado. Simplemente no el que eligieron cuando los programadores de C todavía montaban caballos, portaban armas y tenían 16 KB de memoria
Hans Passant
11
Puede incluir <assert.h>varias veces, con diferentes definiciones de NDEBUG, en el mismo archivo fuente.
Pete Becker
3
En cuanto a #pragma oncesí mismo, hay entornos de hardware (generalmente con unidades en red y posibles rutas múltiples al mismo encabezado) donde no funcionará correctamente.
Pete Becker
12
Si ha #pragma onceasumido, ¿cuál es la forma de contrarrestar ese incumplimiento? #pragma many? ¿Cuántos compiladores han implementado algo así?
Jonathan Leffler

Respuestas:

84

Aquí hay varias preguntas relacionadas:

  • ¿Por qué #pragma onceno se aplica automáticamente?
    Porque hay situaciones en las que desea incluir archivos más de una vez.

  • ¿Por qué querría incluir un archivo varias veces?
    Se han dado varias razones en otras respuestas (Boost.Preprocessor, X-Macros, incluidos los archivos de datos). Me gustaría agregar un ejemplo particular de "evitar la duplicación de código": OpenFOAM fomenta un estilo en el que #includeing bits y piezas dentro de las funciones es un concepto común. Vea, por ejemplo, esta discusión.

  • Bien, pero ¿por qué no es el predeterminado con una opción de exclusión?
    Porque en realidad no está especificado por el estándar. #pragmas son, por definición, extensiones específicas de la implementación.

  • ¿Por qué #pragma onceno se ha convertido todavía en una función estandarizada (ya que cuenta con un amplio soporte)?
    Porque precisar qué es "el mismo archivo" de una manera independiente de la plataforma es sorprendentemente difícil. Consulte esta respuesta para obtener más información .

Max Langhof
fuente
4
En particular, vea este ejemplo para ver un caso en el que pragma oncefalla pero incluir guardias hubiera funcionado. La identificación de archivos por ubicación tampoco funciona porque a veces el mismo archivo ocurre varias veces en un proyecto (por ejemplo, tiene 2 submódulos que incluyen una biblioteca solo de encabezado en sus encabezados y extraen su propia copia)
MM
6
No todos los pragmas son extensiones específicas de la implementación. Por ejemplo, #pragma STDCfamilia . Pero todos controlan el comportamiento definido por la implementación.
Ruslan
3
@ user4581301 Esta respuesta exagera el problema con pragma una vez y no considera los problemas debidos a incluir guardias. En ambos casos se necesita algo de disciplina. Con incluir guardias, uno debe asegurarse de usar un nombre que no se usará en otro archivo (lo que sucederá después de que se modifique una copia de archivo). Con pragma una vez, uno tiene que decidir cuál es el lugar correcto único para su archivo, lo cual es algo bueno después de todo.
Oliv
3
@Mehrdad: ¿Estás sugiriendo en serio que los compiladores escriban en los archivos fuente? Si un compilador ve #ifndef XX, no debe saber si hay algo después del correspondiente #endifhasta que lea el archivo completo. Un compilador que realiza un seguimiento de si el más externo #ifndefincluye todo el archivo y observa qué macro comprueba puede evitar volver a escanear el archivo, pero una directiva para decir que no hay nada de importancia después del punto actual parecería mejor que confiar en los compiladores para recuerda esas cosas.
supercat
38

Puede usar en #include cualquier lugar de un archivo, no solo en el ámbito global, como dentro de una función (y varias veces si es necesario). Seguro, feo y no de buen estilo, pero posible y ocasionalmente sensato (dependiendo del archivo que incluyas). Si #includesolo fuera algo de una sola vez, entonces eso no funcionaría. #includesimplemente hace una sustitución de texto tonta (cortar y pegar) después de todo, y no todo lo que incluye tiene que ser un archivo de encabezado. Puede, por ejemplo, #includeun archivo que contenga datos generados automáticamente que contengan los datos sin procesar para inicializar un std::vector. Me gusta

std::vector<int> data = {
#include "my_generated_data.txt"
}

Y que "my_generated_data.txt" sea algo generado por el sistema de compilación durante la compilación.

O tal vez soy vago / tonto / estúpido y pongo esto en un archivo ( ejemplo muy elaborado):

const noexcept;

y luego lo hago

class foo {
    void f1()
    #include "stupid.file"
    int f2(int)
    #include "stupid.file"
};

Otro ejemplo, un poco menos elaborado, sería un archivo de origen en el que muchas funciones necesitan usar una gran cantidad de tipos en un espacio de nombres, pero no quiere decir simplemente using namespace foo;globalmente, ya que eso contaminaría el espacio de nombres global con muchas otras cosas. no quieres. Así que crea un archivo "foo" que contiene

using std::vector;
using std::array;
using std::rotate;
... You get the idea ...

Y luego haces esto en tu archivo fuente

void f1() {
#include "foo" // needs "stuff"
}

void f2() {
    // Doesn't need "stuff"
}

void f3() {
#include "foo" // also needs "stuff"
}

Nota: No estoy abogando por hacer cosas como esta. Pero es posible y se hace en algunas bases de código y no veo por qué no debería estar permitido. Que no tienen sus usos.

También podría ser que el archivo que incluye se comporte de manera diferente según el valor de ciertas macros #define. Por lo tanto, es posible que desee incluir el archivo en varias ubicaciones, después de haber cambiado primero algún valor, para obtener un comportamiento diferente en diferentes partes de su archivo fuente.

Jesper Juhl
fuente
1
Esto aún funcionaría si todos los encabezados fueran pragma una vez. Siempre que no hayas incluido los datos generados más de una vez.
PSkocik
2
@PSkocik Pero tal vez necesite incluirlo más de una vez. ¿Por qué no debería poder hacerlo?
Jesper Juhl
2
@JesperJuhl Ese es el punto. No necesitará incluirlo más de una vez. Actualmente tiene la opción, pero la alternativa no es mucho peor, en todo caso.
Johnny Cache
9
@PSkocik Si cambio el valor de #defines antes de cada inclusión que cambia el comportamiento del archivo incluido, es muy posible que deba incluirlo varias veces para obtener esos comportamientos diferentes en diferentes partes de mi archivo fuente.
Jesper Juhl
27

Incluir varias veces se puede utilizar, por ejemplo, con la técnica X-macro :

data.inc:

X(ONE)
X(TWO)
X(THREE)

use_data_inc_twice.c

enum data_e { 
#define X(V) V,
   #include "data.inc"
#undef X
};
char const* data_e__strings[]={
#define X(V) [V]=#V,
   #include "data.inc"
#undef X
};

No sé de ningún otro uso.

PSkocik
fuente
Eso suena demasiado complejo. ¿Alguna razón para no incluir esas definiciones en el archivo en primer lugar?
Johnny Cache
2
@JohnnyCache: El ejemplo es una versión simplificada de cómo funcionan las macros X. Por favor lea el enlace; son extremadamente útiles en algunos casos para realizar manipulaciones complejas de datos tabulares. En cualquier uso significativo de X-macros, no habría forma de que pudiera simplemente "incluir esas definiciones en el archivo".
Nicol Bolas
2
@Johnny: sí, una buena razón es garantizar la coherencia (es difícil de hacer a mano cuando solo tiene unas pocas docenas de elementos, sin importar cientos).
Toby Speight
@TobySpeight Heh, supongo que podría ahorrar una sola línea de código para evitar escribir miles en otro lugar. Tiene sentido.
Johnny Cache
1
Para evitar duplicaciones. Especialmente si el archivo es grande. Es cierto que podría usar una macro grande que contenga la lista de macros X, pero dado que los proyectos podrían usar esto, el #pragma oncecomportamiento obligatorio sería un cambio radical.
PSkocik
21

Parece estar operando bajo el supuesto de que el propósito de la función "#include" incluso existente en el lenguaje es proporcionar soporte para la descomposición de programas en múltiples unidades de compilación. Eso es incorrecto.

Puede desempeñar ese papel, pero ese no era su propósito previsto. C se desarrolló originalmente como un lenguaje de nivel ligeramente superior que PDP-11 Macro-11 Assembly para reimplementar Unix. Tenía un preprocesador de macros porque era una característica de Macro-11. Tenía la capacidad de incluir textualmente macros de otro archivo porque esa era una característica de Macro-11 que el Unix existente que estaban portando a su nuevo compilador de C había hecho uso.

Ahora resulta que "#include" es útil para separar el código en unidades de compilación, como (posiblemente) un truco. Sin embargo, el hecho de que existiera este truco significó que se convirtió en el Camino que se hace en C. El hecho de que existiera un camino significaba que no era necesario crear ningún método nuevo para proporcionar esta funcionalidad, por lo que nada más seguro (por ejemplo: no vulnerable a múltiples -inclusión) fue creado. Como ya estaba en C, se copió en C ++ junto con la mayor parte del resto de la sintaxis y modismos de C.

Hay una propuesta para darle a C ++ un sistema de módulos adecuado para que finalmente se pueda prescindir de este truco de preprocesador de 45 años. Sin embargo, no sé qué tan inminente es esto. He escuchado que está en proceso durante más de una década.

TED
fuente
5
Como de costumbre, para comprender C y C ++, debe comprender su historia.
Jack Aidley
Es razonable esperar que los módulos aterricen en febrero.
Davis Herring
7
@DavisHerring - Sí, pero ¿qué febrero?
TED
10

No, esto obstaculizaría significativamente las opciones disponibles para, por ejemplo, los redactores de bibliotecas. Por ejemplo, Boost.Preprocessor permite usar bucles de preprocesador, y la única forma de lograrlos es mediante múltiples inclusiones del mismo archivo.

Y Boost.Preprocessor es un componente básico para muchas bibliotecas muy útiles.

SergeyA
fuente
1
No obstaculizaría nada de eso. OP preguntó sobre un comportamiento predeterminado , no un comportamiento inmutable. Sería muy sensato cambiar el valor predeterminado y en su lugar proporcionar un indicador de preprocesador #pragma reentranto algo por el estilo. La retrospectiva es 20/20.
Konrad Rudolph
Lo obstaculizaría en el sentido de obligar a las personas a actualizar sus bibliotecas y dependencias, @KonradRudolph. No siempre es un problema, pero podría causar problemas con algunos programas heredados. Idealmente, también habría un interruptor de línea de comandos para especificar si el valor predeterminado es onceo reentrant, para mitigar este u otros problemas potenciales.
Justin Time - Reincorpora a Monica
1
@JustinTime Bueno, como dice mi comentario, claramente no es un cambio compatible con versiones anteriores (y, por lo tanto, factible). La pregunta, sin embargo, era por qué se diseñó inicialmente de esa manera, no por qué no se cambiará. Y la respuesta a eso es, sin ambigüedades, que el diseño original fue un gran error con consecuencias de gran alcance.
Konrad Rudolph
8

En el firmware del producto en el que trabajo principalmente, necesitamos poder especificar dónde se deben asignar las funciones y las variables globales / estáticas en la memoria. El procesamiento en tiempo real debe residir en la memoria L1 en el chip para que el procesador pueda acceder a él directamente y rápidamente. El procesamiento menos importante puede ir en la memoria L2 en el chip. Y cualquier cosa que no necesite ser manejada con especial rapidez puede vivir en el DDR externo y pasar por el almacenamiento en caché, porque no importa si es un poco más lento.

El #pragma para asignar a dónde van las cosas es una línea larga y no trivial. Sería fácil equivocarse. El efecto de equivocarse sería que el código / datos se colocarían silenciosamente en la memoria predeterminada (DDR), y el efecto de eso podría ser que el control de ciclo cerrado deje de funcionar sin ninguna razón que sea fácil de ver.

Entonces uso archivos de inclusión, que contienen solo ese pragma. Mi código ahora se ve así.

Archivo de cabecera...

#ifndef HEADERFILE_H
#define HEADERFILE_H

#include "set_fast_storage.h"

/* Declare variables */

#include "set_slow_storage.h"

/* Declare functions for initialisation on startup */

#include "set_fast_storage.h"

/* Declare functions for real-time processing */

#include "set_storage_default.h"

#endif

Y fuente ...

#include "headerfile.h"

#include "set_fast_storage.h"

/* Define variables */

#include "set_slow_storage.h"

/* Define functions for initialisation on startup */

#include "set_fast_storage.h"

/* Define functions for real-time processing */

Notará múltiples inclusiones del mismo archivo allí, incluso solo en el encabezado. Si escribo mal algo ahora, el compilador me dirá que no puede encontrar el archivo de inclusión "set_fat_storage.h" y que puedo arreglarlo fácilmente.

Entonces, en respuesta a su pregunta, este es un ejemplo real y práctico de dónde se requiere la inclusión múltiple.

Graham
fuente
3
Yo diría que su caso de uso es un ejemplo motivador para la _Pragmadirectiva. Los mismos pragmas ahora se pueden expandir a partir de macros normales. Entonces no es necesario incluir más de una vez.
StoryTeller - Unslander Monica