Definición de miembros enteros const estáticos en la definición de clase

109

Tengo entendido que C ++ permite que los miembros constantes estáticos se definan dentro de una clase siempre que sea un tipo entero.

Entonces, ¿por qué el siguiente código me da un error de vinculador?

#include <algorithm>
#include <iostream>

class test
{
public:
    static const int N = 10;
};

int main()
{
    std::cout << test::N << "\n";
    std::min(9, test::N);
}

El error que obtengo es:

test.cpp:(.text+0x130): undefined reference to `test::N'
collect2: ld returned 1 exit status

Curiosamente, si comento la llamada a std :: min, el código se compila y enlaza bien (aunque también se hace referencia a test :: N en la línea anterior).

¿Alguna idea de lo que está pasando?

Mi compilador es gcc 4.4 en Linux.

HighCommander4
fuente
3
Funciona bien en Visual Studio 2010.
Puppy
4
Este error exacto se explica en gcc.gnu.org/wiki/…
Jonathan Wakely
En el caso particular de char, puede definirlo como constexpr static const char &N = "n"[0];. Tenga en cuenta el &. Supongo que esto funciona porque las cadenas literales se definen automáticamente. Sin embargo, estoy un poco preocupado por esto: podría comportarse de manera extraña en un archivo de encabezado entre diferentes unidades de traducción, ya que la cadena probablemente estará en varias direcciones diferentes.
Aaron McDaid
1
Esta pregunta es un manifiesto de cuán pobre es la respuesta de C ++ a "no usar #defines para constantes".
Johannes Overmann
1
@JohannesOvermann En este sentido, quiero mencionar el uso de inline para variables globales desde C ++ 17 inline const int N = 10, que hasta donde yo sé todavía tiene un almacenamiento en algún lugar definido por el enlazador. En este caso, también se podría usar la palabra clave en línea para proporcionar una definición de variable estática dentro de la prueba de definición de clase.
Wormer

Respuestas:

72

Tengo entendido que C ++ permite que los miembros constantes estáticos se definan dentro de una clase siempre que sea un tipo entero.

Tienes razón. Se le permite inicializar integrales constantes estáticas en la declaración de clase, pero esa no es una definición.

http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=/com.ibm.xlcpp8a.doc/language/ref/cplr038.htm

Curiosamente, si comento la llamada a std :: min, el código se compila y enlaza bien (aunque también se hace referencia a test :: N en la línea anterior).

¿Alguna idea de lo que está pasando?

std :: min toma sus parámetros por referencia constante. Si los tomara por valor, no tendría este problema, pero como necesita una referencia, también necesita una definición.

Aquí está el capítulo / verso:

9.4.2 / 4 - Si un staticmiembro de datos es de tipo constintegral o constenumeración, 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 .

Vea la respuesta de Chu para una posible solución.

Edward extraño
fuente
Ya veo, eso es interesante. En ese caso, ¿cuál es la diferencia entre proporcionar el valor en el punto de declaración y proporcionar el valor en el punto de definición? Cual se recomienda?
HighCommander4
Bueno, creo que puede salirse con la suya sin una definición siempre que nunca "use" la variable. Si solo lo usa como parte de una expresión constante, la variable nunca se usa. De lo contrario, no parece haber una gran diferencia además de poder ver el valor en el encabezado, que puede ser o no lo que desea.
Edward Strange
2
La respuesta concisa es estática constante x = 1; es un rvalue pero no un lvalue. El valor está disponible como una constante en tiempo de compilación (puede dimensionar una matriz con él) static const y; [sin inicializador] debe definirse en un archivo cpp y puede usarse como rvalue o lvalue.
Dale Wilson
2
Sería bueno si pudieran ampliar / mejorar esto. Los objetos inicializados pero no definidos deberían, en mi opinión, tratarse de la misma forma que los literales. Por ejemplo, podemos vincular un literal 5a un const int&. Entonces, ¿por qué no tratar los OP test::Ncomo el literal correspondiente?
Aaron McDaid
Interesante explicación, gracias! Esto significa que en C ++ static const int todavía no reemplaza a integer #defines. enum siempre solo tiene el signo int, por lo que hay que usar clases enum para constantes individuales. Sería bastante obvio para mí degenerar una declaración constante con valores constantes y conocidos a una constante literal de qué manera esto se compilaría sin problemas. C ++ tiene un largo camino por recorrer ...
Johannes Overmann
51

El ejemplo de Bjarne Stroustrup en sus preguntas frecuentes de C ++ sugiere que está en lo correcto, y solo necesita una definición si toma la dirección.

class AE {
    // ...
public:
    static const int c6 = 7;
    static const int c7 = 31;
};

const int AE::c7;   // definition

int f()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok
    // ...
}

Dice "Puede tomar la dirección de un miembro estático si (y solo si) tiene una definición fuera de clase" . Lo que sugiere que funcionaría de otra manera. Tal vez su función min invoca direcciones de alguna manera detrás de escena.

HostileFork dice que no confíes en SE
fuente
2
std::mintoma sus parámetros por referencia, por lo que se requiere una definición.
Rakete1111
¿Cómo escribiría la definición si AE es una clase de plantilla AE <clase T> y c7 no es un int sino T :: size_type? Tengo el valor inicializado en "-1" en el encabezado, pero clang dice valor indefinido y no sé cómo escribir la definición.
Fabian
@Fabian Estoy de viaje y por teléfono y estoy un poco ocupado ... pero creo que tu comentario parece que sería mejor escribirlo como una nueva pregunta. Escriba un MCVE que incluya el error que obtiene, y también incluya lo que dice gcc. Apuesto a que la gente te diría rápidamente qué es qué.
HostileFork dice que no confíes en SE
@HostileFork: Al escribir un MCVE, a veces averiguas la solución tú mismo. Para mi caso, la respuesta es template<class K, class V, class C> const typename AE<K,V,C>::KeyContainer::size_type AE<K,V,C>::c7;donde KeyContainer es un typedef de std :: vector <K>. Uno debe listar todos los parámetros de la plantilla y escribir typename porque es un tipo dependiente. Quizás alguien encuentre útil este comentario. Sin embargo, ahora me pregunto cómo exportar esto en una DLL porque la clase de plantilla está, por supuesto, en un encabezado. ¿Necesito exportar c7 ???
Fabian
24

Otra forma de hacer esto, para los tipos enteros de todos modos, es definir constantes como enumeraciones en la clase:

class test
{
public:
    enum { N = 10 };
};
Stephen Chu
fuente
2
Y esto probablemente resolvería el problema. Cuando se utiliza N como parámetro para min (), se creará un temporal en lugar de intentar hacer referencia a una variable supuestamente existente.
Edward Strange
Esto tenía la ventaja de que se puede convertir en privado.
Agostino
11

No solo int. Pero no puede definir el valor en la declaración de clase. Si usted tiene:

class classname
{
    public:
       static int const N;
}

en el archivo .h, entonces debe tener:

int const classname::N = 10;

en el archivo .cpp.

Amardeep AC9MF
fuente
2
Soy consciente de que puede declarar una variable de cualquier tipo dentro de la declaración de clase. Dije que pensaba que las constantes enteras estáticas también podrían definirse dentro de la declaración de clase. ¿No es este el caso? Si no es así, ¿por qué el compilador no da un error en la línea donde trato de definirlo dentro de la clase? Además, ¿por qué la línea std :: cout no causa un error del enlazador, pero la línea std :: min sí?
HighCommander4
No, no se pueden definir miembros estáticos en la declaración de clase porque la inicialización emite código. A diferencia de una función en línea que también emite código, una definición estática es única a nivel mundial.
Amardeep AC9MF
@ HighCommander4: puede proporcionar un inicializador para el static constmiembro integral en la definición de clase. Pero eso todavía no define a ese miembro. Vea la respuesta de Noah Roberts para más detalles.
el AnT
9

Aquí hay otra forma de solucionar el problema:

std::min(9, int(test::N));

(Creo que la respuesta de Crazy Eddie describe correctamente por qué existe el problema).

Karadoc
fuente
5
o inclusostd::min(9, +test::N);
Tomilov Anatoliy
Sin embargo, aquí está la gran pregunta: ¿todo esto es óptimo? No sé ustedes, muchachos, pero mi gran atracción por saltarme la definición es que no debería ocupar memoria ni gastos generales al usar la constante estática.
Opux
6

A partir de C ++ 11 puede utilizar:

static constexpr int N = 10;

En teoría, esto todavía requiere que defina la constante en un archivo .cpp, pero mientras no tome la dirección Nes muy poco probable que la implementación del compilador produzca un error;).

Carlo Wood
fuente
¿Y si necesita pasar el valor como un argumento de tipo 'const int &' como en el ejemplo? :-)
Wormer
Eso funciona bien. No está instanciando N de esa manera, simplemente pasando una referencia constante a un temporal. wandbox.org/permlink/JWeyXwrVRvsn9cBj
Carlo Wood
Tal vez C ++ 17, no C ++ 14, e incluso C ++ 17 en versiones anteriores de gcc 6.3.0 y anteriores, no es algo estándar. Pero gracias por mencionar esto.
Wormer
Ah, sí, tienes razón. No probé c ++ 14 en wandbox. Oh, bueno, esa es la parte donde dije "Esto teóricamente todavía requiere que defina la constante". Entonces, tiene razón en que no es 'estándar'.
Carlo Wood
3

C ++ permite definir miembros estáticos const dentro de una clase

No, 3.1 §2 dice:

Una declaración es una definición a menos que declare una función sin especificar el cuerpo de la función (8.4), contiene el especificador extern (7.1.1) o una especificación de enlace (7.5) y no un inicializador ni un cuerpo de función, declara un dato estático miembro en una definición de clase (9.4), es una declaración de nombre de clase (9.1), es una declaración-enum-opaca (7.2), o es una declaración typedef (7.1.3), una declaración-using (7.3. 3), o una directiva de uso (7.3.4).

fredoverflow
fuente