Respuesta corta: para hacer x
un nombre dependiente, para que la búsqueda se difiera hasta que se conozca el parámetro de la plantilla.
Respuesta larga: cuando un compilador ve una plantilla, se supone que debe realizar ciertas comprobaciones inmediatamente, sin ver el parámetro de la plantilla. Otros se difieren hasta que se conoce el parámetro. Se llama compilación de dos fases, y MSVC no lo hace, pero es requerido por el estándar e implementado por los otros compiladores principales. Si lo desea, el compilador debe compilar la plantilla tan pronto como la vea (a algún tipo de representación interna del árbol de análisis) y diferir la compilación de la instanciación hasta más tarde.
Las comprobaciones que se realizan en la plantilla en sí, en lugar de en instancias particulares de la misma, requieren que el compilador pueda resolver la gramática del código en la plantilla.
En C ++ (y C), para resolver la gramática del código, a veces necesita saber si algo es un tipo o no. Por ejemplo:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
si A es un tipo, eso declara un puntero (sin otro efecto que el de sombrear el global x
). Si A es un objeto, eso es multiplicación (y prohibir la sobrecarga de algunos operadores es ilegal, asignar a un valor r). Si está mal, este error debe diagnosticarse en la fase 1 , está definido por el estándar como un error en la plantilla , no en alguna instancia particular de la misma. Incluso si la plantilla nunca se instancia, si A es un, int
entonces el código anterior está mal formado y debe diagnosticarse, tal como sería si foo
no fuera una plantilla, sino una función simple.
Ahora, el estándar dice que los nombres que no dependen de los parámetros de la plantilla deben poder resolverse en la fase 1. A
Aquí no es un nombre dependiente, se refiere a lo mismo independientemente del tipo T
. Por lo tanto, debe definirse antes de definir la plantilla para poder encontrarla y verificarla en la fase 1.
T::A
sería un nombre que depende de T. No podemos saber en la fase 1 si es un tipo o no. El tipo que eventualmente se utilizará como T
una instanciación probablemente aún no esté definido, e incluso si lo fuera, no sabemos qué tipo (s) se utilizarán como nuestro parámetro de plantilla. Pero tenemos que resolver la gramática para hacer nuestras valiosas verificaciones de fase 1 para plantillas mal formadas. Por lo tanto, el estándar tiene una regla para los nombres dependientes: el compilador debe asumir que no son tipos, a menos que esté calificado typename
para especificar que son tipos o que se usen en ciertos contextos inequívocos. Por ejemplo template <typename T> struct Foo : T::A {};
, en , T::A
se usa como una clase base y, por lo tanto, es inequívocamente un tipo. Si Foo
se instancia con algún tipo que tiene un miembro de datosA
en lugar de un tipo A anidado, es un error en el código que realiza la instanciación (fase 2), no un error en la plantilla (fase 1).
Pero, ¿qué pasa con una plantilla de clase con una clase base dependiente?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
¿Es A un nombre dependiente o no? Con las clases base, cualquier nombre puede aparecer en la clase base. Entonces podríamos decir que A es un nombre dependiente, y tratarlo como un no tipo. Esto tendría el efecto indeseable de que cada nombre en Foo es dependiente y, por lo tanto, cada tipo utilizado en Foo (excepto los tipos incorporados) tiene que ser calificado. Dentro de Foo, tendrías que escribir:
typename std::string s = "hello, world";
porque std::string
sería un nombre dependiente y, por lo tanto, se supone que no es de tipo, a menos que se especifique lo contrario. ¡Ay!
Un segundo problema al permitir su código preferido ( return x;
) es que, incluso si Bar
se definió anteriormente Foo
, y x
no es un miembro en esa definición, alguien podría definir una especialización Bar
para algún tipo Baz
, que Bar<Baz>
sí tiene un miembro de datos x
, y luego crear una instancia Foo<Baz>
. Entonces, en esa instanciación, su plantilla devolvería el miembro de datos en lugar de devolver el global x
. O, por el contrario, si la definición de la plantilla base de Bar
had x
, podrían definir una especialización sin ella, y su plantilla buscaría un x
retorno global Foo<Baz>
. Creo que esto se consideró tan sorprendente y angustiante como el problema que tienes, pero en silencio sorprendente, en lugar de arrojar un error sorprendente.
Para evitar estos problemas, el estándar en efecto dice que las clases base dependientes de plantillas de clase simplemente no se consideran para la búsqueda a menos que se solicite explícitamente. Esto evita que todo sea dependiente solo porque se puede encontrar en una base dependiente. También tiene el efecto indeseable que estás viendo: tienes que calificar cosas de la clase base o no se encuentra. Hay tres formas comunes de hacer A
dependiente:
using Bar<T>::A;
en la clase: A
ahora se refiere a algo en Bar<T>
, por lo tanto, dependiente.
Bar<T>::A *x = 0;
en el punto de uso: una vez más, A
definitivamente está en Bar<T>
. Esta es una multiplicación ya que typename
no se usó, por lo que posiblemente sea un mal ejemplo, pero tendremos que esperar hasta la instanciación para saber si operator*(Bar<T>::A, x)
devuelve un valor. Quién sabe, tal vez sí ...
this->A;
en el punto de uso: A
es un miembro, por lo que si no está en Foo
, debe estar en la clase base, nuevamente el estándar dice que esto lo hace dependiente.
La compilación de dos fases es complicada y difícil, e introduce algunos requisitos sorprendentes para la palabrería adicional en su código. Pero más bien, como la democracia, es probablemente la peor forma posible de hacer las cosas, aparte de todas las demás.
Podría argumentar razonablemente que, en su ejemplo, return x;
no tiene sentido six
es un tipo anidado en la clase base, por lo que el lenguaje debería (a) decir que es un nombre dependiente y (2) tratarlo como un no tipo, y su código funcionaría sin this->
. Hasta cierto punto, usted es víctima del daño colateral de la solución a un problema que no se aplica en su caso, pero aún existe el problema de que su clase base posiblemente introduzca nombres debajo de usted que oscurecen los globales, o no tener nombres que pensó. tenían, y un ser global encontrado en su lugar.
También podría argumentar que el valor predeterminado debería ser el opuesto para los nombres dependientes (asumir el tipo a menos que de alguna manera se especifique que es un objeto), o que el valor predeterminado debería ser más sensible al contexto (en std::string s = "";
, std::string
podría leerse como un tipo ya que nada más hace gramatical sentido, aunque std::string *s = 0;
sea ambiguo). De nuevo, no sé exactamente cómo se acordaron las reglas. Supongo que la cantidad de páginas de texto que se requerirían, mitigadas contra la creación de muchas reglas específicas para qué contextos toman un tipo y cuáles no.