resolucion - poo c++ ejemplos
¿Por qué tengo que acceder a los miembros de la clase base de la plantilla a través de este puntero? (3)
La x
está oculta durante la herencia. Puede mostrar a través de:
template <typename T>
class derived : public base<T> {
public:
using base<T>::x; // added "using" statement
int f() { return x; }
};
Si las siguientes clases no son plantillas, simplemente podría tener x
en la clase derived
. Sin embargo, con el siguiente código, 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 hacer que x
un nombre dependiente, de modo que la búsqueda se difiere 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 (para 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 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 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 sombrear la x
global). Si A es un objeto, eso es una multiplicación (y salvo que la sobrecarga de un operador sea ilegal, se asigna 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 una instancia particular de la misma. Incluso si la plantilla nunca se crea una instancia, si A es un int
entonces el código anterior está mal formado y debe ser diagnosticado, 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 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 usará como T
en una instancia muy probablemente aún no está definido, e incluso si lo fuera, no sabemos qué tipo (s) se usará (n) como parámetro de nuestra 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 nombres dependientes: el compilador debe suponer que no son tipos, a menos que califique con typename
para especificar que sean tipos, o que se usen en ciertos contextos no ambiguos. Por ejemplo, en la template <typename T> struct Foo : T::A {};
, T::A
se utiliza como una clase base y, por lo tanto, es inequívocamente un tipo. Si Foo
se instancia con algún tipo que tenga un miembro de datos A
lugar de un tipo A anidado, eso 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 podría aparecer en la clase base. Entonces podríamos decir que A es un nombre dependiente, y tratarlo como 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) debe ser calificado. Dentro de Foo, deberías 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 que Foo
, y x
no es miembro de esa definición, alguien más tarde podría definir una especialización de Bar
para algún tipo Baz
, tal que Bar<Baz>
tiene un miembro de datos x
, y luego crea una instancia de Foo<Baz>
. Entonces, en esa instanciación, su plantilla devolvería el miembro de datos en lugar de devolver la x
global. O, por el contrario, si la definición de la plantilla base de Bar
tuviera x
, podrían definir una especialización sin él, y su plantilla buscaría una x
global para regresar en Foo<Baz>
. Creo que esto fue considerado tan sorprendente y angustiante como el problema que tienes, pero es silenciosamente 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 buscan nombres a menos que los nombres ya sean dependientes por alguna otra razón. Esto evita que todo sea dependiente solo porque se puede encontrar en una base dependiente. También tiene el efecto indeseable que está viendo: tiene que calificar cosas de la clase base o no se encuentra. Hay tres formas comunes de hacer que A
dependa:
-
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
está definitivamente enBar<T>
. Esto es una multiplicación ya quetypename
no se usó, por lo que posiblemente sea un mal ejemplo, pero tendremos que esperar hasta la creación de instancias para averiguar si eloperator*(Bar<T>::A, x)
devuelve un valor r. Quién sabe, quizás sí ... -
this->A;
en el punto de uso:A
es miembro, por lo tanto, si no está enFoo
, debe estar en la clase base; de nuevo, 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 verborrea extra en su código. Pero, al igual que 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 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 no-tipo, y su código funcionaría sin this->
. En cierta medida, usted es víctima de un daño colateral de la solución a un problema que no se aplica en su caso, pero aún existe la cuestión de que su clase base podría introducir nombres debajo de usted que sombrean los globales o que no tiene nombres que usted 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 (suponga el tipo a menos que 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 tiene sentido gramatical, aunque std::string *s = 0;
es ambigua). Nuevamente, no sé exactamente cómo se acordaron las reglas. Supongo que el número de páginas de texto que se necesitarían mitigaba la creación de muchas reglas específicas para las que los contextos toman un tipo y los que 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 clase base no se examina durante la búsqueda de nombres no calificados en el punto de definición de la plantilla de clase o miembro o durante una instancia de la plantilla de clase o miembro.
Si "porque el estándar lo dice" cuenta como una respuesta, no lo sé. Ahora podemos preguntarnos por qué el estándar exige eso, pero como la excelente respuesta de Steve Jessop y otros señalan, la respuesta a esta última pregunta es bastante larga y discutible. Desafortunadamente, cuando se trata del Estándar de C ++, a menudo es casi imposible dar una explicación breve y autónoma de por qué el estándar exige algo; esto se aplica a la última pregunta también.