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

11 minutos de lectura

¿Por que tengo que acceder a los miembros de la
Ali

Si las clases a continuación no fueran plantillas, simplemente podría tener x en el derived clase. Sin embargo, con el siguiente código, yo tengo que utilizar 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;
}

  • @Ed Swangren: Lo siento, me lo perdí entre las respuestas ofrecidas al publicar esta pregunta. Había estado buscando la respuesta durante mucho tiempo antes de eso.

    – Alí

    10 de enero de 2011 a las 1:58

  • Esto sucede debido a la búsqueda de nombres en dos fases (que no todos los compiladores usan de forma predeterminada) y los nombres dependientes. Hay 3 soluciones a este problema, aparte de anteponer el x con this->a saber: 1) Usa el prefijo base<T>::x, 2) Agregar una declaración using base<T>::x, 3) Utilice un conmutador de compilador global que habilite el modo permisivo. Las ventajas y desventajas de estas soluciones se describen en stackoverflow.com/questions/50321788/…

    – Jorge Robinson

    14 mayo 2018 a las 13:53


Respuesta corta: para hacer x un nombre dependiente, de modo 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 de inmediato, sin ver el parámetro de la plantilla. Otros se difieren hasta que se conoce el parámetro. Se llama compilación en 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 (en algún tipo de representación de árbol de análisis interno) y aplazar la compilación de instancias hasta más tarde.

Las comprobaciones que se realizan en la plantilla en sí, en lugar de instancias particulares de la misma, requieren que el compilador sea capaz de resolver la gramática del código en la plantilla.

En C++ (y C), para resolver la gramática del código, a veces necesitas 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 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 ser diagnosticado. en fase 1está definido por el estándar como un error en la plantilla, no en alguna instancia particular de ello. Incluso si la plantilla nunca se instancia, si A es un int entonces el código anterior está mal formado y debe ser diagnosticado, tal como sería si foo no era una plantilla en absoluto, sino una función simple.

Ahora, el estándar dice que los nombres que no son dependiente de los parámetros de la plantilla debe 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, debe definirse antes de que se defina 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 creación de instancias, es muy probable que aún no esté definida, 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 hacer nuestras valiosas comprobaciones de fase 1 para 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 se califiquen con typename para especificar que ellos están tipos, o usados ​​en ciertos contextos inequívocos. por ejemplo en template <typename T> struct Foo : T::A {};, T::A se utiliza como clase base y, por lo tanto, es inequívocamente un tipo. Si Foo se instancia con algún tipo que tiene un miembro de datos A en lugar de un tipo A anidado, 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; }
};

¿A es un nombre de dependiente o no? Con clases base, ninguna El nombre podría aparecer en la clase base. Así que 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 un tipo a menos que se especifique lo contrario. ¡Ay!

Un segundo problema con permitir su código preferido (return x;) es que aunque Bar se define antes Fooy x no es miembro de esa definición, alguien podría definir más tarde una especialización de Bar para algún tipo Baztal que Bar<Baz> tiene un miembro de datos xy luego instanciar Foo<Baz>. Entonces, en esa instancia, 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 tenido xpodrían definir una especialización sin ella, y su plantilla buscaría una especialización global x volver en Foo<Baz>. Creo que esto se consideró tan sorprendente y angustioso 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 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 solo porque podría encontrarse en una base dependiente. También tiene el efecto indeseable que está viendo: debe calificar cosas de la clase base o no se encuentran. 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 – Nuevamente, A definitivamente está en Bar<T>. Esta es la multiplicación ya que typename no se usó, por lo que posiblemente sea un mal ejemplo, pero tendremos que esperar hasta la creación de instancias para averiguar si operator*(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, así que si no está en Foodebe 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 verborrea adicional 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 un no tipo, y su código funcionaría sin this->. Hasta cierto punto, usted es víctima de daños colaterales por la solución de un problema que no se aplica en su caso, pero aún existe el problema de que su clase base podría introducir nombres debajo de usted que ensombrecen los globales, o no tener nombres que pensó. tenían, y en su lugar se encontró un ser global.

También podría argumentar que el valor predeterminado debería ser el opuesto para los nombres dependientes (suponga que el tipo a menos que se especifique de alguna manera 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 ambiguo). Nuevamente, 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, mitigado contra la creación de muchas reglas específicas para qué contextos toman un tipo y cuáles no.

  • Ooh, buena respuesta detallada. Aclarado un par de cosas que nunca me he molestado en buscar. 🙂 +1

    – jalf

    10 de enero de 2011 a las 3:41

  • @jalf: ¿existe algo así como C++QTWBFAETYNSYEWTKTAAHMITTBGOW – “Preguntas que se harían con frecuencia, excepto que no está seguro de querer saber la respuesta y tiene cosas más importantes con las que seguir adelante”?

    –Steve Jessop

    10 de enero de 2011 a las 3:51


  • respuesta extraordinaria, me pregunto si la pregunta podría caber en las preguntas frecuentes.

    – Matthieu M.

    10 de enero de 2011 a las 8:19

  • Vaya, ¿podemos decir enciclopédico? Cinco altos Sin embargo, un punto sutil: “Si se crea una instancia de Foo con algún tipo que tenga 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 ).” Podría ser mejor decir que la plantilla no tiene un formato incorrecto, pero esto aún podría ser un caso de una suposición incorrecta o un error de lógica por parte del escritor de la plantilla. Si la creación de instancias marcada fuera realmente el caso de uso previsto, entonces la plantilla sería incorrecta.

    – Ionoclasta Brigham

    12 de julio de 2013 a las 19:53


  • @JuanH. Dado que varios compiladores implementan -fpermissive o similar, sí es posible. No conozco los detalles de cómo se implementa, pero el compilador debe estar postergando la resolución x hasta que conozca la clase base de plantilla real T. 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 T, y cuando la búsqueda tenga éxito, emita el texto que sugiere. Sería una sugerencia muy acertada si solo se hace en los casos en que funciona: las posibilidades de que el usuario se refiera a otra x desde otro ámbito son bastante pequeños!

    –Steve Jessop

    25 mayo 2020 a las 22:47


¿Por que tengo que acceder a los miembros de la
Ali

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


Actualizar: 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, ya sea en el punto de definición de la plantilla de clase o miembro o durante una instanciación de la plantilla de clase o el miembro.

Si “porque la norma lo dice” cuenta como respuesta, no lo sé. Ahora podemos preguntar 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 e independiente de por qué el estándar exige algo; esto se aplica a la última pregunta también.

los x se 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; }
};

  • Esta respuesta no explica por qué esta escondido

    – jamesdlin

    10 de enero de 2011 a las 2:49

  • yo obtengo base<T> is not a namespace or unscoped enum

    – JDługosz

    29 sep 2020 a las 15:31

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad