Referencia no definida a static const int

79

Hoy me encontré con un problema interesante. Considere este simple ejemplo:

template <typename T>
void foo(const T & a) { /* code */ }

// This would also fail
// void foo(const int & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

int main()
{
   Bar b;
   b.func();
}

Al compilar me sale un error:

Undefined reference to 'Bar::kConst'

Ahora, estoy bastante seguro de que esto se debe a static const intque no está definido en ninguna parte, lo cual es intencional porque, según tengo entendido, el compilador debería poder realizar el reemplazo en tiempo de compilación y no necesitar una definición. Sin embargo, dado que la función toma un const int &parámetro, parece que no realiza la sustitución, sino que prefiere una referencia. Puedo resolver este problema realizando el siguiente cambio:

foo(static_cast<int>(kConst));

Creo que esto ahora está obligando al compilador a hacer un int temporal y luego pasar una referencia a eso, lo que puede hacer con éxito en tiempo de compilación.

Me preguntaba si esto fue intencional, o estoy esperando demasiado de gcc para poder manejar este caso. ¿O es algo que no debería estar haciendo por alguna razón?

JaredC
fuente
1
En la práctica, podría obtener const int kConst = 1;el mismo resultado. Además, rara vez hay una razón (no puedo pensar en ninguna) para que una función tome un parámetro del tipo const int &, solo use un intaquí.
Björn Pollex
1
@Space, la función real era una plantilla, editaré mi pregunta para mencionar eso.
JaredC
1
@Space fyi, no hacerlo staticda un error `ISO C ++ prohíbe la inicialización del miembro 'kConst' ... haciendo que 'kConst' sea estático. '
JaredC
Mi mal, gracias por la corrección.
Björn Pollex
1
Lo molesto es, este error puede mostrar en usos inocuos como std::min( some_val, kConst), ya que std::min<T>tiene parámetros de tipo T const &, y la implicación es que tenemos que pasar una referencia a la kConst. Descubrí que ocurría solo cuando la optimización estaba desactivada. Se corrigió usando un yeso estático.
Greggo

Respuestas:

61

Es intencional, 9.4.2 / 4 dice:

Si un miembro de datos estáticos es de tipo enumeración const integral o const, 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 aún estará definido en un ámbito de espacio de nombres si se usa en el programa

Cuando pasa el miembro de datos estáticos por referencia constante, lo "usa", 3.2 / 2:

Una expresión se evalúa potencialmente a menos que aparezca donde se requiere una expresión constante integral (vea 5.19), sea el operando del operador sizeof (5.3.3), o sea el operando del operador typeid y la expresión no designe un valor l de tipo de clase polimórfica (5.2.8). Se utiliza un objeto o una función no sobrecargada si su nombre aparece en una expresión potencialmente evaluada.

Entonces, de hecho, lo "usa" cuando lo pasa por valor también, o en un static_cast. Es solo que GCC lo ha dejado libre en un caso, pero no en el otro.

[Editar: gcc está aplicando las reglas de los borradores de C ++ 0x: "Una función variable o no sobrecargada cuyo nombre aparece como una expresión potencialmente evaluada se usa odr a menos que sea un objeto que satisfaga los requisitos para aparecer en una expresión (5.19) y se aplica inmediatamente la conversión de lvalor a rvalue (4.1). ". La conversión estática realiza la conversión lvalue-rvalue inmediatamente, por lo que en C ++ 0x no se "usa".]

El problema práctico con la referencia constante es que footiene derecho a tomar la dirección de su argumento y compararla, por ejemplo, con la dirección del argumento de otra llamada, almacenada en un global. Dado que un miembro de datos estáticos es un objeto único, esto significa que si llama foo(kConst)desde dos TU diferentes, la dirección del objeto pasado debe ser la misma en cada caso. AFAIK GCC no puede arreglar eso a menos que el objeto esté definido en una (y solo una) TU.

Bien, en este caso fooes una plantilla, por lo tanto, la definición es visible en todas las TU, por lo que quizás el compilador podría, en teoría, descartar el riesgo de que haga algo con la dirección. Pero, en general, no debería tomar direcciones o referencias a objetos inexistentes ;-)

Steve Jessop
fuente
1
Gracias por el ejemplo de tomar la dirección de la referencia. Creo que esa es una razón práctica real por la que el compilador no hace lo que espero.
JaredC
Para una conformidad completa, creo que tendrías que definir algo como template <int N> int intvalue() { return N; }. Entonces con intvalue<kConst>, kConstsolo aparece en un contexto que requiere una expresión constante integral, por lo que no se usa. Pero la función devuelve un temporal con el mismo valor que kConst, y que se puede vincular a una referencia constante. Sin embargo, no estoy seguro de que haya una forma más sencilla de hacer cumplir de forma portátil que kConstno se utiliza.
Steve Jessop
1
Experimento el mismo problema al usar dicha variable constante estática en un operador ternario (es decir, algo así como r = s ? kConst1 : kConst2) con gcc 4.7. Lo resolví usando un archivo real if. De todos modos gracias por la respuesta!
Clodéric
2
... y std :: min / std :: max, ¡lo que me llevó aquí!
sabio
"AFAIK GCC no puede arreglar eso a menos que el objeto esté definido en una (y sólo una) TU". Es una lástima, ya que ya es posible hacer eso con constantes: compílelas varias veces como 'defs débiles' en .rodata, y luego haga que el vinculador solo elija una, lo que garantiza que todas las referencias reales tengan la misma dirección. Esto es realmente lo que se hace para typeid; Sin embargo, puede fallar de formas extrañas cuando se utilizan bibliotecas compartidas.
Greggo
27

Si está escribiendo una variable const estática con un inicializador dentro de la declaración de clase, es como si hubiera escrito

class Bar
{
      enum { kConst = 1 };
}

y GCC lo tratará de la misma manera, lo que significa que no tiene dirección.

El código correcto debe ser

class Bar
{
      static const int kConst;
}
const int Bar::kConst = 1;
pelya
fuente
Gracias por este ejemplo ilustrativo.
shuhalo
12

Este es un caso realmente válido. Especialmente porque foo podría ser una función de STL como std :: count que toma una constante T & como su tercer argumento.

Pasé mucho tiempo tratando de entender por qué el enlazador tenía problemas con un código tan básico.

El mensaje de error

Referencia no definida a 'Bar :: kConst'

nos dice que el enlazador no puede encontrar un símbolo.

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 U Bar::kConst

Podemos ver en la 'U' que Bar :: kConst no está definido. Por tanto, cuando el enlazador intenta hacer su trabajo, tiene que encontrar el símbolo. Pero solo declara kConst y no lo define.

La solución en C ++ también es definirlo de la siguiente manera:

template <typename T>
void foo(const T & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

const int Bar::kConst;       // Definition <--FIX

int main()
{
   Bar b;
   b.func();
}

Luego, puede ver que el compilador colocará la definición en el archivo objeto generado:

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 R Bar::kConst

Ahora, puede ver la 'R' que dice que está definida en la sección de datos.

Stac
fuente
¿Está bien que aparezca una constante en la salida "nm-C" dos veces, primero con una "R" y una dirección, y luego con una "U"?
quant_dev
¿Tienes algún ejemplo? En el ejemplo proporcionado, >nm -C main.o | grep kConstsolo me da una línea 0000000000400644 R Bar::kConst.
Stac
Lo veo al compilar una biblioteca estática.
quant_dev
1
En este caso, ¡por supuesto! Una biblioteca estática es solo un agregado de archivos de objeto. El enlace solo lo realiza el cliente de la biblioteca estática. Entonces, si coloca el archivo de almacenamiento, el archivo de objeto que contiene la definición de const y otro archivo de objeto que llama a Bar :: func (), verá el símbolo una vez con la definición y una vez sin: nm -C lib.ale da Constants.o: 0000000000000000 R Bar::kConsty main_file.o: U Bar::kConst ....
Stac
2

g ++ versión 4.3.4 acepta este código (ver este enlace ). Pero la versión 4.4.0 de g ++ lo rechaza.

TonyK
fuente
2

Creo que este artefacto de C ++ significa que cada vez que Bar::kConstse hace referencia, se usa su valor literal en su lugar.

Esto significa que en la práctica no hay ninguna variable a la que hacer referencia.

Puede que tenga que hacer esto:

void func()
{
  int k = kConst;
  foo(k);
}
quamrana
fuente
Esto es básicamente lo que estaba logrando al cambiarlo foo(static_cast<int>(kConst));, ¿verdad?
JaredC
2

También puede reemplazarlo por una función miembro constexpr:

class Bar
{
  static constexpr int kConst() { return 1; };
};
Yoav
fuente
Tenga en cuenta que esto requiere 4 líneas en la declaración y llaves después de la "constante", por lo que termina escribiendo foo = std :: numeric_limits <int> :: max () * bar :: this_is_a_constant_that_looks_like_a_method () y esperando que su estándar de codificación supere y el optimizador lo corrige por ti.
Code Abominator
1

Truco simple: use +antes de kConstpasar la función. Esto evitará que se tome una referencia a la constante, y de esta manera el código no generará una solicitud de enlazador al objeto constante, sino que continuará con el valor de la constante en tiempo del compilador.

Ethouris
fuente
Sin embargo, es una pena que el compilador no emita una advertencia cuando se toma una dirección de un static constvalor que se inicializa en la declaración. Esto siempre daría lugar a un error del vinculador, y cuando la misma constante también se declara por separado en un archivo de objeto, esto también sería un error. El compilador también es plenamente consciente de la situación.
Ethouris
¿Cuál es la mejor forma de rechazar referencias? Actualmente estoy haciendo static_cast<decltype(kConst)>(kConst).
Velkan
@Velkan Me gustaría saber cómo hacer eso también. Tu truco tatic_cast <decltype (kConst)> (kConst) no funciona en el caso de que kConst sea un char [64]; obtiene "error: static_cast from 'char *' to 'decltype (start_time)' (también conocido como 'char [64]') no está permitido".
Don Hatch
@DonHatch, no me gusta la arqueología del software, pero, por lo que recuerdo, es muy difícil hacer funcionar una matriz sin procesar mediante la copia. Entonces, sintácticamente, la foo()pregunta de la pregunta original necesitará la dirección, y no hay ningún mecanismo para tratarla como una copia temporal de toda la matriz.
Velkan
0

Experimenté el mismo problema mencionado por Cloderic (const estática en un operador ternario :) r = s ? kConst1 : kConst2, pero solo se quejó después de desactivar la optimización del compilador (-O0 en lugar de -Os). Ocurrió en gcc-none-eabi 4.8.5.

Scg
fuente