¿Por qué tengo que acceder a los miembros de la clase base de la plantilla a través de este puntero?
Si las clases a continuación no fueran plantillas, simplemente podría tenerlas x
en la derived
clase. Sin embargo, con el código siguiente, tengo que usar this->x
. ¿Por qué?
template <typename T>
class base {
protected:
int x;
};
template <typename T>
class derived : public base<T> {
public:
int f() { return this->x; }
};
int main() {
derived<int> d;
d.f();
return 0;
}
Respuesta corta: para crear x
un nombre dependiente, de modo que la búsqueda se posponga 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 aplazan hasta que se conozca el parámetro. Se llama compilación en dos fases y MSVC no lo hace, pero lo requiere el estándar y lo implementan los otros compiladores principales. Si lo desea, el compilador debe compilar la plantilla tan pronto como la vea (en algún tipo de representación de árbol de análisis interno) y posponer la compilación de la instanciación hasta más tarde.
Las comprobaciones que se realizan en la propia plantilla, 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 es necesario 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, salvo que algún operador lo sobrecargue, es ilegal asignarlo a un valor r). Si es incorrecto, este error debe diagnosticarse en la fase 1 ; el estándar lo define como un error en la plantilla , no en alguna instancia particular de la misma. Incluso si nunca se crea una instancia de la plantilla, si A es un int
entonces el código anterior está mal formado y debe diagnosticarse, tal como lo sería si foo
no fuera una plantilla en absoluto, 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 hay un nombre dependiente, se refiere a lo mismo independientemente del tipo T
. Por lo tanto, es necesario definirlo antes de definir la plantilla para poder encontrarlo y verificarlo en la fase 1.
T::A
sería un nombre que depende de T. No es posible que sepamos en la fase 1 si es un tipo o no. Es muy probable que el tipo que eventualmente se usará T
en una creación de instancias ni siquiera esté definido todavía, e incluso si lo estuviera, no sabemos qué tipo(s) se usarán como nuestro parámetro de plantilla. Pero tenemos que resolver la gramática para poder realizar nuestras preciosas comprobaciones de la fase 1 en busca de plantillas mal formadas. Entonces, el estándar tiene una regla para los nombres dependientes: el compilador debe asumir que no son tipos, a menos que estén calificados para typename
especificar que son tipos , o se usan en ciertos contextos inequívocos. Por ejemplo, en template <typename T> struct Foo : T::A {};
, T::A
se utiliza como clase base y, por tanto, es inequívocamente un tipo. Si Foo
se crea una instancia con algún tipo que tiene un miembro de datos A
en lugar de un tipo A anidado, eso es un error en el código que realiza la creación de instancias (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 podría aparecer en la clase base. Entonces podríamos decir que A es un nombre dependiente y tratarlo como si no fuera de tipo. Esto tendría el efecto no deseado de que cada nombre en Foo es dependiente y, por lo tanto, cada tipo utilizado en Foo (excepto los tipos integrados) debe estar 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 un 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
está definido antes Foo
y x
no es miembro de esa definición, alguien podría definir más tarde una especialización Bar
para algún tipo Baz
, de modo que Bar<Baz>
tenga 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 archivo global x
. O por el contrario, si la definición de plantilla base Bar
tuviera x
, podrían definir una especialización sin ella, y su plantilla buscaría un global x
para regresar Foo<Baz>
. Creo que esto se consideró tan sorprendente y angustiante como el problema que usted tiene, pero es silenciosamente sorprendente, en lugar de arrojar un error sorprendente.
Para evitar estos problemas, el estándar vigente dice que las clases base dependientes de las plantillas de clase simplemente no se consideran para la búsqueda a menos que se solicite explícitamente. Esto evita que todo sea dependiente sólo porque podría encontrarse 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 encuentran. Hay tres formas comunes de volverse A
dependiente:
using Bar<T>::A;
en la clase:A
ahora se refiere a algo enBar<T>
, por lo tanto, dependiente.Bar<T>::A *x = 0;
en el punto de uso: nuevamente,A
definitivamente está enBar<T>
. Esta es una multiplicación ya quetypename
no se usó, por lo que posiblemente sea un mal ejemplo, pero tendremos que esperar hasta la instanciación para saber sioperator*(Bar<T>::A, x)
devuelve un valor r. Quién sabe, tal vez sí...this->A;
en el punto de uso,A
es un miembro, por lo que si no está enFoo
, debe estar en la clase base; nuevamente, el estándar dice que esto lo hace dependiente.
La compilación en dos fases es complicada y difícil, e introduce algunos requisitos sorprendentes para una mayor palabrería en el código. Pero, al igual que la democracia, es probablemente la peor manera posible de hacer las cosas, aparte de todas las demás.
Se podría argumentar razonablemente que en su ejemplo, return x;
no tiene sentido si x
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 si no fuera un tipo, y su código funcionaría sin this->
. Hasta cierto punto, usted es víctima de un daño colateral debido a la solución a un problema que no se aplica en su caso, pero aún existe el problema de que su clase base pueda introducir nombres debajo de usted que ensombrezcan a los globales, o no tener nombres que pensaba. tenían, y en su lugar se encontró un ser global.
También se podría argumentar que el valor predeterminado debería ser lo opuesto para los nombres dependientes (se supone que es un tipo a menos que se especifique de alguna manera que sea 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 que la gramática sea correcta). sentido, aunque std::string *s = 0;
sea ambiguo). Una vez más, no sé muy bien cómo se acordaron las reglas. Mi conjetura es que la cantidad de páginas de texto que se requerirían, mitigaba la creación de muchas reglas específicas para qué contextos toman un tipo y cuáles no.
(Respuesta original del 10 de enero de 2011)
Creo que encontré la respuesta: Problema de GCC: usar un miembro de una clase base que depende de un argumento de plantilla . La respuesta no es específica de gcc.
Actualización: en respuesta al comentario de mmichael , del borrador N3337 del estándar C++11:
14.6.2 Nombres dependientes [temp.dep]
[...]
3 En la definición de una clase o plantilla de clase, si una clase base depende de un parámetro de plantilla, el alcance de la clase base no se examina durante la búsqueda de nombres no calificados ni en el punto de definición de la plantilla o miembro de clase o durante una instanciación de la plantilla o miembro de clase.
Si "porque el estándar lo dice" cuenta como respuesta, no lo sé. Ahora podemos preguntarnos por qué el estándar exige eso, pero como señalan la excelente respuesta de Steve Jessop y otros, la respuesta a esta última pregunta es bastante larga y discutible. Desafortunadamente, cuando se trata del estándar C++, a menudo es casi imposible dar una explicación breve y completa de por qué el estándar exige algo; Esto también se aplica a la última pregunta.