¿Por qué los miembros de datos estáticos deben definirse fuera de la clase por separado en C ++ (a diferencia de Java)?

41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

No veo la necesidad de haber A::xdefinido por separado en un archivo .cpp (o el mismo archivo para plantillas). ¿Por qué no se puede A::xdeclarar y definir al mismo tiempo?

¿Ha sido prohibido por razones históricas?

Mi pregunta principal es, ¿afectará alguna funcionalidad si staticlos miembros de datos se declararon / definieron al mismo tiempo (igual que Java )?

iammilind
fuente
Como práctica recomendada, generalmente es mejor ajustar su variable estática en un método estático (posiblemente como estático local) para evitar problemas de orden de inicialización.
Tamás Szelei
2
Esta regla se relaja un poco en C ++ 11. los miembros estáticos const generalmente ya no tienen que definirse. Ver: en.wikipedia.org/wiki/…
mirk
44
@afishwhoswimsaround: no es una buena idea especificar reglas generalizadas para todas las situaciones (las mejores prácticas deben aplicarse con contexto). Aquí está tratando de resolver un problema que no existe. El problema del orden de inicialización solo afecta a los objetos que tienen constructores y acceden a otros objetos de duración de almacenamiento estático. Como 'x' es int, el primero no se aplica, ya que 'x' es privado, el segundo no se aplica. En tercer lugar, esto no tiene nada que ver con la pregunta.
Martin York
1
¿Pertenece al desbordamiento de pila?
Lightness compite con Monica el
2
C ++ 17 permite la inicialización en línea de miembros de datos estáticos (incluso para los tipos no enteros): inline static int x[] = {1, 2, 3};. Ver en.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Respuestas:

15

Creo que la limitación que ha considerado no está relacionada con la semántica (¿por qué debería cambiar algo si la inicialización se definió en el mismo archivo?) Sino con el modelo de compilación de C ++ que, por razones de compatibilidad con versiones anteriores, no se puede cambiar fácilmente porque o se vuelve demasiado complejo (admite un nuevo modelo de compilación y el existente al mismo tiempo) o no permitiría compilar el código existente (al introducir un nuevo modelo de compilación y descartar el existente).

El modelo de compilación C ++ se deriva del de C, en el que importa declaraciones en un archivo fuente al incluir archivos (encabezados). De esta manera, el compilador ve exactamente un gran archivo fuente, que contiene todos los archivos incluidos, y todos los archivos incluidos de esos archivos, de forma recursiva. Esto tiene una gran ventaja para IMO, es decir, hace que el compilador sea más fácil de implementar. Por supuesto, puede escribir cualquier cosa en los archivos incluidos, es decir, declaraciones y definiciones. Es una buena práctica colocar declaraciones en archivos de encabezado y definiciones en archivos .c o .cpp.

Por otro lado, es posible tener un modelo de compilación en el que el compilador sabe muy bien si está importando la declaración de un símbolo global que se define en otro módulo , o si está compilando la definición de un símbolo global proporcionada por El módulo actual . Solo en el último caso, el compilador debe poner este símbolo (por ejemplo, una variable) en el archivo de objeto actual.

Por ejemplo, en GNU Pascal puede escribir una unidad aen un archivo a.pascomo este:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

donde la variable global se declara e inicializa en el mismo archivo fuente.

Entonces puede tener diferentes unidades que importen MyStaticVariableay usen la variable global , por ejemplo, una unidad b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

y una unidad c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Finalmente, puede usar las unidades byc en un programa principal m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Puede compilar estos archivos por separado:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

y luego produce un ejecutable con:

$ gpc -o m m.o a.o b.o c.o

y ejecutarlo:

$ ./m
1
2
3

El truco aquí es que cuando el compilador ve una directiva de usos en un módulo de programa (por ejemplo, usa a en b.pas), no incluye el archivo .pas correspondiente, sino que busca un archivo .gpi, es decir, un precompilado archivo de interfaz (ver la documentación ). El .gpicompilador genera estos archivos junto con los .oarchivos cuando se compila cada módulo. Por lo tanto, el símbolo global MyStaticVariablesolo se define una vez en el archivo objeto a.o.

Java funciona de manera similar: cuando el compilador importa una clase A a la clase B, mira el archivo de clase para A y no necesita el archivo A.java. Por lo tanto, todas las definiciones e inicializaciones para la clase A se pueden colocar en un archivo fuente.

Volviendo a C ++, la razón por la cual en C ++ tiene que definir miembros de datos estáticos en un archivo separado está más relacionada con el modelo de compilación de C ++ que con las limitaciones impuestas por el enlazador u otras herramientas utilizadas por el compilador. En C ++, importar algunos símbolos significa construir su declaración como parte de la unidad de compilación actual. Esto es muy importante, entre otras cosas, debido a la forma en que se compilan las plantillas. Pero esto implica que no puede / no debe definir ningún símbolo global (funciones, variables, métodos, miembros de datos estáticos) en un archivo incluido, de lo contrario, estos símbolos podrían definirse de manera múltiple en los archivos de objetos compilados.

Giorgio
fuente
42

Como los miembros estáticos se comparten entre TODAS las instancias de una clase, deben definirse en un solo lugar. Realmente, son variables globales con algunas restricciones de acceso.

Si intenta definirlos en el encabezado, se definirán en cada módulo que incluya ese encabezado, y obtendrá errores durante el enlace ya que encuentra todas las definiciones duplicadas.

Sí, este es al menos en parte un problema histórico que data de cfront; se podría escribir un compilador que creara una especie de "static_members_of_everything.cpp" oculto y se vincule a eso. Sin embargo, rompería la compatibilidad con versiones anteriores y no habría ningún beneficio real al hacerlo.

mjfgates
fuente
2
Mi pregunta no es la razón del comportamiento actual, sino más bien la justificación de dicha gramática del lenguaje. En otras palabras, suponga que si las staticvariables se declaran / definen en el mismo lugar (como Java), ¿qué puede salir mal?
iammilind
8
@iammilind Creo que no entiendes que la gramática es necesaria debido a la explicación de esta respuesta. ¿Ahora por qué? Debido al modelo de compilación de C (y C ++): los archivos c y cpp son el archivo de código real que se compila por separado como programas separados, luego se vinculan entre sí para hacer un ejecutable completo. Los encabezados no son realmente código para el compilador, son solo texto para copiar y pegar dentro de los archivos c y cpp. Ahora, si algo se define varias veces, no puede compilarlo, de la misma manera que no se compilará si tiene varias variables locales con el mismo nombre.
Klaim
1
@Klaim, ¿qué pasa con los staticmiembros template? Se permiten en todos los archivos de encabezado, ya que deben estar visibles. No estoy disputando esta respuesta, pero tampoco coincide con mi pregunta.
iammilind
Las plantillas de @iammilind no son código real, son códigos que generan código. Cada instancia de una plantilla tiene una y solo una instancia estática de cada declaración estática que proporciona el compilador. Todavía tiene que definir la instancia, pero al definir una plantilla de una instancia, no es un código real, como se dijo anteriormente. Las plantillas son, literalmente, plantillas de código para que el compilador genere código.
Klaim
2
@iammilind: las plantillas generalmente se instancian en cada archivo de objeto, incluidas sus variables estáticas. En Linux con archivos de objetos ELF, el compilador marca las instancias como símbolos débiles , lo que significa que el vinculador combina múltiples copias de la misma instanciación. La misma tecnología podría usarse para permitir la definición de variables estáticas en los archivos de encabezado, por lo que la razón por la que no se hace es probablemente una combinación de razones históricas y consideraciones de rendimiento de compilación. Se espera que todo el modelo de compilación se arregle una vez que el próximo estándar C ++ incorpore módulos .
Han
6

La razón probable de esto es que esto mantiene el lenguaje C ++ implementable en entornos donde el archivo de objeto y el modelo de vinculación no admite la fusión de múltiples definiciones de múltiples archivos de objeto.

Una declaración de clase (llamada declaración por buenas razones) se extrae en varias unidades de traducción. Si la declaración contuviera definiciones para variables estáticas, terminaría con múltiples definiciones en múltiples unidades de traducción (Y recuerde, estos nombres tienen un enlace externo).

Esa situación es posible, pero requiere que el enlazador maneje múltiples definiciones sin quejarse.

(Y tenga en cuenta que esto entra en conflicto con la Regla de una definición, a menos que se pueda hacer de acuerdo con el tipo de símbolo o en qué tipo de sección se coloca).

Kaz
fuente
6

Hay una gran diferencia entre C ++ y Java.

Java opera en su propia máquina virtual que crea todo en su propio entorno de tiempo de ejecución. Si una definición se ve más de una vez, simplemente actuará sobre el mismo objeto que el entorno de ejecución conoce en última instancia.

En C ++ no hay un "propietario del conocimiento final": C ++, C, Fortran Pascal, etc. son todos "traductores" de un código fuente (archivo CPP) a un formato intermedio (el archivo OBJ o ".o", dependiendo de OS) donde las declaraciones se traducen en instrucciones de máquina y los nombres se convierten en direcciones indirectas mediadas por una tabla de símbolos.

El compilador no crea un programa, sino otro programa (el "enlazador"), que une todos los OBJ-s (sin importar el idioma del que proceden) volviendo a señalar todas las direcciones que están hacia símbolos, hacia sus definición efectiva

Por cierto, el enlazador funciona, una definición (lo que crea el espacio físico para una variable) debe ser única.

Tenga en cuenta que C ++ no se vincula por sí mismo, y que el vinculador no es emitido por las especificaciones de C ++: el vinculador existe debido a la forma en que se construyen los módulos del sistema operativo (generalmente en C y ASM). C ++ tiene que usarlo como está.

Ahora: un archivo de encabezado es algo para "pegar" en varios archivos CPP. Cada archivo CPP se traduce independientemente de todos los demás. Un compilador que traduce diferentes archivos CPP, todos los que reciben una misma definición colocará el " código de creación " para el objeto definido en todos los OBJ resultantes.

El compilador no sabe (y nunca sabrá) si todos esos OBJ alguna vez se usarán juntos para formar un solo programa o por separado para formar diferentes programas independientes.

El vinculador no sabe cómo y por qué existen las definiciones y de dónde provienen (ni siquiera sabe acerca de C ++: cada "lenguaje estático" puede producir definiciones y referencias para vincular). Simplemente sabe que hay referencias a un "símbolo" dado que está "definido" en una dirección resultante dada.

Si hay varias definiciones (no confunda las definiciones con las referencias) para un símbolo dado, el enlazador no tiene conocimiento (siendo independiente del lenguaje) sobre qué hacer con ellos.

Es como fusionar varias ciudades para formar una gran ciudad: si te encuentras con dos " Time square " y un número de personas que vienen de fuera pidiendo ir a " Time square ", no puedes decidir sobre una base técnica pura. (sin ningún conocimiento sobre la política que asignó esos nombres y se encargará de administrarlos) en qué lugar exacto enviarlos.

Emilio Garavaglia
fuente
3
La diferencia entre Java y C ++ con respecto a los símbolos globales no está relacionada con que Java tenga una máquina virtual, sino con el modelo de compilación de C ++. A este respecto, no pondría a Pascal y C ++ en la misma categoría. En cambio, agruparía C y C ++ como "lenguajes en los que las declaraciones importadas se incluyen y compilan junto con el archivo fuente principal" en lugar de Java y Pascal (y quizás OCaml, Scala, Ada, etc.) como "lenguajes en los que El compilador busca las declaraciones importadas en archivos precompilados que contienen información sobre los símbolos exportados ".
Giorgio
1
@Giorgio: la referencia a Java puede no ser bienvenida, pero creo que la respuesta de Emilio es mayormente correcta al llegar al meollo del problema, es decir, la fase del archivo de objeto / enlazador después de una compilación separada.
ixache
5

Es necesario porque de lo contrario el compilador no sabe dónde colocar la variable. Cada archivo cpp se compila individualmente y no conoce el otro. El enlazador resuelve variables, funciones, etc. Personalmente, no veo cuál es la diferencia entre los miembros vtable y los estáticos (no tenemos que elegir en qué archivo se definen los vtable).

Principalmente supongo que es más fácil para los escritores de compiladores implementarlo de esa manera. Existen variables estáticas fuera de la clase / estructura y quizás por razones de coherencia o porque sería "más fácil de implementar" para los escritores de compiladores que definieron esa restricción en los estándares.


fuente
2

Creo que encontré la razón. La definición de staticvariable en un espacio separado permite inicializarla a cualquier valor. Si no se inicializa, el valor predeterminado será 0.

Antes de C ++ 11, la inicialización en clase no estaba permitida en C ++. Entonces uno no puede escribir como:

struct X
{
  static int i = 4;
};

Por lo tanto, ahora para inicializar la variable, uno debe escribirla fuera de la clase como:

struct X
{
  static int i;
};
int X::i = 4;

Como se discutió en otras respuestas también, int X::iahora es global y la declaración global en muchos archivos causa un error de enlace de múltiples símbolos.

Por lo tanto, uno tiene que declarar una staticvariable de clase dentro de una unidad de traducción separada. Sin embargo, aún se puede argumentar que la siguiente forma debería indicar al compilador que no cree múltiples símbolos

static int X::i = 4;
^^^^^^
iammilind
fuente
0

A :: x es solo una variable global pero un espacio de nombres para A, y con restricciones de acceso.

Alguien todavía tiene que declararlo, como cualquier otra variable global, y eso incluso puede hacerse en un proyecto que está estáticamente vinculado al proyecto que contiene el resto del código A.

Llamaría a todos estos mal diseño, pero hay algunas características que puede explotar de esta manera:

  1. el orden de llamada del constructor ... No es importante para un int, pero para un miembro más complejo que tal vez accede a otras variables estáticas o globales, puede ser crítico.

  2. el inicializador estático: puede dejar que un cliente decida a qué A :: x se debe inicializar.

  3. en c ++ y c, debido a que tiene acceso completo a la memoria a través de punteros, la ubicación física de las variables es significativa. Hay cosas muy traviesas que puede explotar en función de dónde se encuentra una variable en un objeto de enlace.

Dudo que estos sean "por qué" ha surgido esta situación. Probablemente sea solo una evolución de C convirtiéndose en C ++, y un problema de compatibilidad con versiones anteriores que le impide cambiar el idioma ahora.

James Podesta
fuente
2
esto no parece ofrecer nada sustancial sobre los puntos de hecho y explicado en anteriores 6 respuestas
mosquito