¿Por qué tengo que acceder a los miembros de la clase base de la plantilla a través de este puntero?

Resuelto Ali asked hace 13 años • 3 respuestas

Si las clases a continuación no fueran plantillas, simplemente podría tenerlas xen la derivedclase. 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;
}
Ali avatar Jan 10 '11 08:01 Ali
Aceptado

Respuesta corta: para crear xun 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 intentonces el código anterior está mal formado y debe diagnosticarse, tal como lo sería si foono 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. AAquí 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::Aserí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á Ten 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 typenameespecificar que son tipos , o se usan en ciertos contextos inequívocos. Por ejemplo, en template <typename T> struct Foo : T::A {};, T::Ase utiliza como clase base y, por tanto, es inequívocamente un tipo. Si Foose crea una instancia con algún tipo que tiene un miembro de datos Aen 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::stringserí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 Barestá definido antes Fooy xno es miembro de esa definición, alguien podría definir más tarde una especialización Barpara 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 Bartuviera x, podrían definir una especialización sin ella, y su plantilla buscaría un global xpara 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 Adependiente:

  • using Bar<T>::A;en la clase: Aahora se refiere a algo en Bar<T>, por lo tanto, dependiente.
  • Bar<T>::A *x = 0;en el punto de uso: nuevamente, Adefinitivamente está en Bar<T>. Esta es una multiplicación ya que typenameno 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 r. Quién sabe, tal vez sí...
  • this->A;en el punto de uso, Aes 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 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 xes 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::stringpodrí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.

Steve Jessop avatar Jan 10 '2011 02:01 Steve Jessop

(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.

Ali avatar Jan 10 '2011 01:01 Ali