Si las siguientes clases no fueran plantillas, simplemente podría tenerlas x
en la derived
clase. 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;
}
x
conthis->
, a saber: 1) Usar el prefijobase<T>::x
, 2) Agregar una declaraciónusing base<T>::x
, 3) Usar un conmutador de compilador global que habilite el modo permisivo. Los pros y los contras de estas soluciones se describen en stackoverflow.com/questions/50321788/…Respuestas:
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:
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 sifoo
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 tipoT
. 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á comoT
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é calificadotypename
para especificar que son tipos o que se usen en ciertos contextos inequívocos. Por ejemplotemplate <typename T> struct Foo : T::A {};
, en ,T::A
se usa como una clase base y, por lo tanto, es inequívocamente un tipo. SiFoo
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?
¿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:
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 siBar
se definió anteriormenteFoo
, yx
no es un miembro en esa definición, alguien podría definir una especializaciónBar
para algún tipoBaz
, queBar<Baz>
sí tiene un miembro de datosx
, y luego crear una instanciaFoo<Baz>
. Entonces, en esa instanciación, su plantilla devolvería el miembro de datos en lugar de devolver el globalx
. O, por el contrario, si la definición de la plantilla base deBar
hadx
, podrían definir una especialización sin ella, y su plantilla buscaría unx
retorno globalFoo<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 enBar<T>
, por lo tanto, dependiente.Bar<T>::A *x = 0;
en el punto de uso: una vez más,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. 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 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 sinthis->
. 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, aunquestd::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.fuente
-fpermissive
o similar, sí, es posible. No sé los detalles de cómo se implementa, pero el compilador debe diferir la resoluciónx
hasta que conozca la clase base tempate realT
. Entonces, en principio, en modo no permisivo, podría registrar el hecho de que lo ha diferido, diferirlo, hacer la búsqueda una vez que lo haya hechoT
, y cuando la búsqueda tenga éxito, emita el texto que sugiere. Sería una sugerencia muy precisa si solo se hace en los casos en que funciona: ¡las posibilidades de que el usuario se refiriera a otrox
de otro alcance son muy pequeñas!(Respuesta original del 10 de enero de 2011)
Creo que he encontrado 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:
Si "porque el estándar lo dice" cuenta como una respuesta, no lo sé. Ahora podemos preguntar 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 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 también a la última pregunta.
fuente
El
x
está oculto durante la herencia. Puede mostrar a través de:fuente