¿Por qué no puedo inicializar un miembro estático no constante o una matriz estática en la clase?

116

¿Por qué no puedo inicializar un staticmiembro o staticmatriz no constante en una clase?

class A
{
    static const int a = 3;
    static int b = 3;
    static const int c[2] = { 1, 2 };
    static int d[2] = { 1, 2 };
};

int main()
{
    A a;

    return 0;
}

el compilador emite los siguientes errores:

g++ main.cpp
main.cpp:4:17: error: ISO C++ forbids in-class initialization of non-const static member b
main.cpp:5:26: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:5:33: error: invalid in-class initialization of static data member of non-integral type const int [2]’
main.cpp:6:20: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:6:27: error: invalid in-class initialization of static data member of non-integral type int [2]’

Tengo dos preguntas:

  1. ¿Por qué no puedo inicializar staticmiembros de datos en clase?
  2. ¿Por qué no puedo inicializar staticmatrices en clase, incluso la constmatriz?
Yishu Fang
fuente
1
Creo que la razón principal es que es complicado hacerlo bien. En principio, probablemente podría hacer lo que está hablando, pero habría algunos efectos secundarios extraños. Como si su ejemplo de matriz estuviera permitido, entonces puede obtener el valor de A :: c [0], pero no pasar A :: c a una función ya que eso requeriría una dirección y tiempo de compilación las constantes no tienen dirección. C ++ 11 ha habilitado algo de esto mediante el uso de constexpr.
Vaughn Cato
Gran pregunta y respuesta hecha. Enlace que me ayudó: msdn.microsoft.com/en-us/library/0e5kx78b.aspx
ETFovac

Respuestas:

144

¿Por qué no puedo inicializar staticmiembros de datos en clase?

El estándar C ++ permite que solo se inicialicen tipos de enumeración o integrales constantes estáticas dentro de la clase. Esta es la razón por la que ase permite la inicialización mientras que otras no.

Referencia:
C ++ 03 9.4.2 Miembros de datos estáticos
§4

Si un miembro de datos estáticos es de tipo enumeración constante o integral, su declaración en la definición de clase puede especificar un inicializador constante que será una expresión constante integral (5.19). En ese caso, el miembro puede aparecer en expresiones constantes integrales. El miembro seguirá estando definido en un ámbito de espacio de nombres si se utiliza en el programa y la definición del ámbito de espacio de nombres no deberá contener un inicializador.

¿Qué son los tipos integrales?

C ++ 03 3.9.1 Tipos fundamentales
§7

Los tipos bool, char, wchar_t y los tipos enteros con signo y sin signo se denominan colectivamente tipos integrales.43) Un sinónimo de tipo integral es tipo entero.

Nota:

43) Por tanto, las enumeraciones (7.2) no son integrales; sin embargo, las enumeraciones se pueden promover a int, unsigned int, long o unsigned long, como se especifica en 4.5.

Solución alterna:

Puede usar el truco de la enumeración para inicializar una matriz dentro de la definición de su clase.

class A 
{
    static const int a = 3;
    enum { arrsize = 2 };

    static const int c[arrsize] = { 1, 2 };

};

¿Por qué la Norma no lo permite?

Bjarne explica esto acertadamente aquí :

Por lo general, una clase se declara en un archivo de encabezado y un archivo de encabezado generalmente se incluye en muchas unidades de traducción. Sin embargo, para evitar reglas complicadas del enlazador, C ++ requiere que cada objeto tenga una definición única. Esa regla se rompería si C ++ permitiera la definición en clase de entidades que deben almacenarse en la memoria como objetos.

¿Por qué solo se static constpermiten tipos integrales y enumeraciones en clase?

La respuesta está oculta en la cita de Bjarne, léala atentamente,
"C ++ requiere que cada objeto tenga una definición única. Esa regla se rompería si C ++ permitiera la definición en clase de las entidades que deben almacenarse en la memoria como objetos".

Tenga en cuenta que solo los static constnúmeros enteros pueden tratarse como constantes de tiempo de compilación. El compilador sabe que el valor entero no cambiará en ningún momento y, por lo tanto, puede aplicar su propia magia y aplicar optimizaciones, el compilador simplemente integra dichos miembros de clase, es decir, ya no se almacenan en la memoria, ya que se elimina la necesidad de ser almacenados en la memoria. , otorga a tales variables la excepción a la regla mencionada por Bjarne.

Vale la pena señalar aquí que incluso si los static constvalores integrales pueden tener In-Class Initialization, no se permite tomar la dirección de tales variables. Se puede tomar la dirección de un miembro estático si (y solo si) tiene una definición fuera de clase. Esto valida aún más el razonamiento anterior.

Las enumeraciones están permitidas porque se pueden usar valores de un tipo enumerado donde se esperan ints. ver cita anterior


¿Cómo cambia esto en C ++ 11?

C ++ 11 relaja la restricción hasta cierto punto.

C ++ 11 9.4.2 Miembros de datos estáticos
§3

Si un miembro de datos estáticos es de tipo literal const, su declaración en la definición de clase puede especificar un inicializador-llave-o-igual en el que cada cláusula-inicializadora que es una expresión-asignación es una expresión constante. Un miembro de datos estáticos de tipo literal se puede declarar en la definición de clase con el constexpr specifier;si es así, su declaración especificará un inicializador de llave o igual en el que cada cláusula de inicializador que es una expresión de asignaciónes una expresión constante. [Nota: en ambos casos, el miembro puede aparecer en expresiones constantes. —Nota final] El miembro aún se definirá en un ámbito de espacio de nombres si se utiliza en el programa y la definición del ámbito de espacio de nombres no debe contener un inicializador.

También, C ++ 11 será permitir (§12.6.2.8) un miembro de datos no estático para inicializar donde se declara (en su clase). Esto significará una semántica de usuario mucho más sencilla.

Tenga en cuenta que estas funciones aún no se han implementado en la última versión de gcc 4.7, por lo que es posible que aún obtenga errores de compilación.

Alok Save
fuente
7
Las cosas son diferentes en c ++ 11. La respuesta podría necesitar una actualización.
bames53
4
Esto no parece ser cierto: "Tenga en cuenta que solo los enteros const estáticos pueden tratarse como constantes de tiempo de compilación. El compilador sabe que el valor entero no cambiará en ningún momento y, por lo tanto, puede aplicar su propia magia y aplicar optimizaciones, el compilador simplemente inserta dichos miembros de clase, es decir, ya no se almacenan en la memoria , " ¿Está seguro de que no necesariamente se almacenan en la memoria? ¿Qué pasa si proporciono definiciones para los miembros? ¿Qué &membervolvería?
Nawaz
2
@Als: Sí. Esa es mi pregunta. Entonces, ¿por qué C ++ permite la inicialización en clase solo para tipos integrales? Su respuesta no responde correctamente. Piense en por qué no permite la inicialización del static const char*miembro.
Nawaz
3
@Nawaz: Debido a que C ++ 03 solo permitía el inicializador constante para el tipo de enumeración estática y const integral y const y ningún otro tipo, C ++ 11 extiende esto a un tipo literal const que relaja las normas para la inicialización en clase. en C ++ 03 fue quizás un descuido que justificó un cambio y, por lo tanto, se corrigió en C ++ 11, si existen razones tácticas tradicionales para el cambio, no las conozco. ellos.
Alok Save
4
El par de "Solución alternativa" que mencionó no funciona con g ++.
iammilind
4

Esto parece una reliquia de los viejos tiempos de los enlazadores simples. Puede utilizar variables estáticas en métodos estáticos como solución:

// header.hxx
#include <vector>

class Class {
public:
    static std::vector<int> & replacement_for_initialized_static_non_const_variable() {
        static std::vector<int> Static {42, 0, 1900, 1998};
        return Static;
    }
};

int compilation_unit_a();

y

// compilation_unit_a.cxx
#include "header.hxx"

int compilation_unit_a() {  
    return Class::replacement_for_initialized_static_non_const_variable()[1]++;
}

y

// main.cxx
#include "header.hxx"

#include <iostream>

int main() {
    std::cout
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << std::endl;
}

construir:

g++ -std=gnu++0x -save-temps=obj -c compilation_unit_a.cxx 
g++ -std=gnu++0x -o main main.cxx compilation_unit_a.o

correr:

./main

El hecho de que esto funcione (de manera consistente, incluso si la definición de la clase está incluida en diferentes unidades de compilación), muestra que el enlazador actual (gcc 4.9.2) es lo suficientemente inteligente.

Divertido: imprime 0123en el brazo y 3210en x86.

no-un-usuario
fuente
1

Creo que es para evitar que mezcles declaraciones y definiciones. (Piense en los problemas que podrían ocurrir si incluye el archivo en varios lugares).

usuario541686
fuente
0

Es porque solo puede haber una definición de la A::aque usan todas las unidades de traducción.

Si se desempeñó static int a = 3;en una clase en un encabezado incluido en todas las unidades de traducción, obtendría múltiples definiciones. Por lo tanto, la definición no fuera de línea de una estática se convierte forzosamente en un error del compilador.

Usar static inlineo static constremediar esto. static inlinesolo concretiza el símbolo si se usa en la unidad de traducción y asegura que el enlazador solo selecciona y deja una copia si está definido en múltiples unidades de traducción debido a que está en un grupo comdat. constEl alcance del archivo hace que el compilador nunca emita un símbolo porque siempre se sustituye inmediatamente en el código a menos que externse use, lo cual no está permitido en una clase.

Una cosa a tener en cuenta es que static inline int b;se trata como una definición, mientras que static const int bo static const A b;todavía se tratan como una declaración y deben definirse fuera de línea si no lo define dentro de la clase. Curiosamente, static constexpr A b;se trata como una definición, mientras que static constexpr int b;es un error y debe tener un inicializador (esto se debe a que ahora se convierten en definiciones y, como cualquier definición const / constexpr en el alcance del archivo, requieren un inicializador que un int no tiene sino un tipo de clase lo hace porque tiene un implícito = A()cuando es una definición - clang permite esto, pero gcc requiere que se inicialice explícitamente o es un error. Esto no es un problema con inline en su lugar). static const A b = A();no está permitido y debe ser constexproinlinepara permitir un inicializador para un objeto estático con tipo de clase, es decir, hacer un miembro estático de tipo de clase más que una declaración. Entonces, sí en ciertas situaciones A a;no es lo mismo que inicializar explícitamente A a = A();(la primera puede ser una declaración, pero si solo se permite una declaración para ese tipo, la última es un error. La última solo se puede usar en una definición. Lo constexprconvierte en una definición ). Si usa constexpry especifica un constructor predeterminado, entonces el constructor deberá serconstexpr

#include<iostream>

struct A
{
    int b =2;
    mutable int c = 3; //if this member is included in the class then const A will have a full .data symbol emitted for it on -O0 and so will B because it contains A.
    static const int a = 3;
};

struct B {
    A b;
    static constexpr A c; //needs to be constexpr or inline and doesn't emit a symbol for A a mutable member on any optimisation level
};

const A a;
const B b;

int main()
{
    std::cout << a.b << b.b.b;
    return 0;
}

Un miembro estático es una declaración de alcance de archivo simple extern int A::a;(que solo se puede hacer en la clase y las definiciones fuera de línea deben hacer referencia a un miembro estático en una clase y deben ser definiciones y no pueden contener extern) mientras que un miembro no estático es parte de la definición de tipo completa de una clase y tienen las mismas reglas que las declaraciones de alcance de archivo sin extern. Son definiciones implícitas. También lo int i[]; int i[5];es una redefinición, mientras static int i[]; int A::i[5];que no lo es, pero a diferencia de 2 externos, el compilador aún detectará un miembro duplicado si lo hace static int i[]; static int i[5];en la clase.

Lewis Kelsey
fuente
-3

las variables estáticas son específicas de una clase. Los constructores inicializan los atributos ESPECIALMENTE para una instancia.


fuente