Referencia indefinida a miembro de clase estática

201

¿Alguien puede explicar por qué el siguiente código no se compilará? Al menos en g ++ 4.2.4.

Y más interesante, ¿por qué se compilará cuando envíe MIEMBRO a int?

#include <vector>

class Foo {  
public:  
    static const int MEMBER = 1;  
};

int main(){  
    vector<int> v;  
    v.push_back( Foo::MEMBER );       // undefined reference to `Foo::MEMBER'
    v.push_back( (int) Foo::MEMBER ); // OK  
    return 0;
}
Pawel Piatkowski
fuente
Edité la pregunta para sangrar el código por cuatro espacios en lugar de usar <pre> <code> </code> </pre>. Esto significa que los corchetes angulares no se interpretan como HTML.
Steve Jessop
stackoverflow.com/questions/16284629/… Puede consultar esta pregunta.
Tooba Iqbal

Respuestas:

199

En realidad, debe definir el miembro estático en alguna parte (después de la definición de clase). Prueba esto:

class Foo { /* ... */ };

const int Foo::MEMBER;

int main() { /* ... */ }

Eso debería eliminar la referencia indefinida.

Drew Hall
fuente
3
Buen punto, la inicialización de enteros constantes estáticos en línea crea una constante entera con alcance de la que no puede tomar la dirección, y vector toma un parámetro de referencia.
Evan Teran el
10
Esta respuesta solo aborda la primera parte de la pregunta. La segunda parte es mucho más interesante: ¿por qué agregar un molde NOP hace que funcione sin requerir la declaración externa?
nobar 01 de
32
Acabo de pasar un buen tiempo descubriendo que si la definición de clase está en un archivo de encabezado, la asignación de la variable estática debería estar en el archivo de implementación, no en el encabezado.
shanet
1
@shanet: Muy buen punto, ¡debería haberlo mencionado en mi respuesta!
Drew Hall
Pero si lo declaro como constante, ¿no me es posible cambiar el valor de esa variable?
Namratha
78

El problema surge debido a un interesante choque de nuevas características de C ++ y lo que estás tratando de hacer. Primero, echemos un vistazo a la push_backfirma:

void push_back(const T&)

Se espera una referencia a un objeto de tipo T. Bajo el antiguo sistema de inicialización, dicho miembro existe. Por ejemplo, el siguiente código compila muy bien:

#include <vector>

class Foo {
public:
    static const int MEMBER;
};

const int Foo::MEMBER = 1; 

int main(){
    std::vector<int> v;
    v.push_back( Foo::MEMBER );       // undefined reference to `Foo::MEMBER'
    v.push_back( (int) Foo::MEMBER ); // OK  
    return 0;
}

Esto se debe a que hay un objeto real en algún lugar que tiene ese valor almacenado en él. Sin embargo, si cambia al nuevo método de especificar miembros const estáticos, como lo hizo anteriormente, Foo::MEMBERya no es un objeto. Es una constante, algo similar a:

#define MEMBER 1

Pero sin los dolores de cabeza de una macro de preprocesador (y con seguridad de tipo). Eso significa que el vector, que espera una referencia, no puede obtenerlo.

Douglas Mayle
fuente
2
gracias, eso ayudó ... eso podría calificar para stackoverflow.com/questions/1995113/strangest-language-feature si aún no está allí ...
Andre Holzner
1
También vale la pena señalar que MSVC acepta la versión no emitida sin quejas.
porges
44
-1: Esto simplemente no es cierto. Todavía se supone que debes definir miembros estáticos inicializados en línea, cuando se usan odr en alguna parte. Que las optimizaciones del compilador puedan deshacerse de su error de enlazador no cambia eso. En este caso, su conversión lvalue-to-rvalue (gracias al (int)reparto) se produce en la unidad de traducción con una visibilidad perfecta de la constante, y Foo::MEMBERya no se utiliza odr . Esto está en contraste con la primera llamada a la función, donde se pasa una referencia y se evalúa en otro lugar.
Carreras de ligereza en órbita el
¿Qué hay de void push_back( const T& value );? const&Se pueden unir con valores.
Kostas
59

El estándar C ++ requiere una definición para su miembro const estático si la definición es de alguna manera necesaria.

Se requiere la definición, por ejemplo, si se utiliza su dirección. push_backtoma su parámetro por referencia constante, por lo que estrictamente el compilador necesita la dirección de su miembro y usted debe definirla en el espacio de nombres.

Cuando expulsa explícitamente la constante, está creando un temporal y es este temporal el que está vinculado a la referencia (bajo reglas especiales en el estándar).

Este es un caso realmente interesante, y realmente creo que vale la pena plantear un problema para que se cambie el estándar para que tenga el mismo comportamiento para su miembro constante.

Aunque, de una manera extraña, esto podría verse como un uso legítimo del operador unario '+'. Básicamente, el resultado de unary +es un valor r y, por lo tanto, se aplican las reglas para vincular los valores r con las referencias constantes y no utilizamos la dirección de nuestro miembro constante estático:

v.push_back( +Foo::MEMBER );
Richard Corden
fuente
3
+1. Sí, es ciertamente extraño que para un objeto x de tipo T, la expresión "(T) x" se pueda usar para unir una constante const, mientras que "x" no se puede. ¡Me encanta tu observación sobre "unary +"! ¿Quién hubiera pensado que el pobre pequeño "unario +" realmente tenía un uso ... :)
j_random_hacker
3
Pensando en el caso general ... ¿Hay algún otro tipo de objeto en C ++ que tenga la propiedad de que (1) se puede usar como un valor l solo si se ha definido, pero (2) se puede convertir en un valor r sin ser definido?
j_random_hacker
Buena pregunta, y al menos por el momento no puedo pensar en ningún otro ejemplo. Probablemente esto solo se deba a que el comité estaba reutilizando la sintaxis existente.
Richard Corden
@RichardCorden: ¿cómo resolvió eso el unario +?
Blood-HaZaRd
1
@ Blood-HaZaRd: antes de que rvalue hace referencia, la única sobrecarga de push_backera a const &. El uso del miembro resultó directamente en que el miembro estuviera vinculado a la referencia, lo que requería que tuviera una dirección. Sin embargo, agregar el +crea un temporal con el valor del miembro. La referencia luego se vincula a ese temporal en lugar de requerir que el miembro tenga una dirección.
Richard Corden
10

Aaa.h

class Aaa {

protected:

    static Aaa *defaultAaa;

};

Aaa.cpp

// You must define an actual variable in your program for the static members of the classes

static Aaa *Aaa::defaultAaa;
iso9660
fuente
1

No tengo idea de por qué funciona el elenco, pero Foo :: MEMBER no se asigna hasta la primera vez que se carga Foo, y como nunca lo estás cargando, nunca se asigna. Si tuviera una referencia a un Foo en alguna parte, probablemente funcionaría.

Paul Tomblin
fuente
Creo que estás respondiendo tu propia pregunta: el elenco funciona porque crea una referencia (temporal).
Jaap Versteegh
1

Con C ++ 11, lo anterior sería posible para tipos básicos como

class Foo {
public:  
  static constexpr int MEMBER = 1;  
};

La constexprparte crea una expresión estática en lugar de una variable estática , y eso se comporta como una definición de método en línea extremadamente simple. Sin embargo, el enfoque resultó un poco tambaleante con los constexprs de C-string dentro de las clases de plantilla.

Starturtle
fuente
Esto resultó ser esencial para mí, porque el "static const int MEMBER = 1;" es necesario para usar MEMBER en los conmutadores, mientras que la declaración externa es necesaria para usarlo en vectores, y no puede tener ambos al mismo tiempo. Pero la expresión que se da aquí hace el trabajo para los dos, al menos con mi compilador.
Ben Farmer
0

Con respecto a la segunda pregunta: push_ref toma la referencia como parámetro, y no puede tener una referencia al miembro de const estático de una clase / estructura. Una vez que llame a static_cast, se crea una variable temporal. Y se puede pasar una referencia a este objeto, todo funciona bien.

O al menos mi colega que resolvió esto lo dijo.

Quarra
fuente