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

199

Si las siguientes clases no fueran plantillas, simplemente podría tenerlas xen la derivedclase. 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;
}
Ali
fuente
1
Ah cielos. Tiene algo que ver con la búsqueda de nombres. Si alguien no responde esto pronto, lo buscaré y lo publicaré (ocupado ahora).
templatetypedef
@Ed Swangren: Lo siento, me perdí entre las respuestas ofrecidas al publicar esta pregunta. Había estado buscando la respuesta durante mucho tiempo antes de eso.
Ali
66
Esto sucede debido a la búsqueda de nombres en dos fases (que no todos los compiladores usan por defecto) y nombres dependientes. Hay 3 soluciones a este problema, además de prefijar xcon this->, a saber: 1) Usar el prefijo base<T>::x, 2) Agregar una declaración using 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/…
George Robinson

Respuestas:

274

Respuesta corta: para hacer xun 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:

#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 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, intentonces el código anterior está mal formado y debe diagnosticarse, tal como sería si foono 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. AAquí 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::Aserí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á como Tuna 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é calificado typenamepara especificar que son tipos o que se usen en ciertos contextos inequívocos. Por ejemplo template <typename T> struct Foo : T::A {};, en , T::Ase usa como una clase base y, por lo tanto, es inequívocamente un tipo. Si Foose 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?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

¿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:

typename std::string s = "hello, world";

porque std::stringserí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 si Barse definió anteriormente Foo, y xno es un miembro en esa definición, alguien podría definir una especialización Barpara algún tipo Baz, que Bar<Baz>sí tiene 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 global x. O, por el contrario, si la definición de la plantilla base de Barhad x, podrían definir una especialización sin ella, y su plantilla buscaría un xretorno global Foo<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 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: una vez más, 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. 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 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 sin this->. 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::stringpodría leerse como un tipo ya que nada más hace gramatical sentido, aunque std::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.

Steve Jessop
fuente
1
Ooh, buena respuesta detallada. Aclaré un par de cosas que nunca me he molestado en buscar. :) +1
jalf
20
@jalf: ¿existe algo como C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Preguntas que se formularían con frecuencia, excepto que no estás seguro de querer saber la respuesta y tener cosas más importantes con las que seguir adelante"?
Steve Jessop
44
respuesta extraordinaria, me pregunto si la pregunta podría encajar en las preguntas frecuentes.
Matthieu M.
Whoa, ¿podemos decir enciclopédico? Sin embargo, un punto sutil: "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 hace la instanciación (fase 2), no un error en la plantilla (fase 1) ". Podría ser mejor decir que la plantilla no está mal formada, pero esto podría ser un caso de una suposición incorrecta o un error lógico por parte del escritor de la plantilla. Si la instanciación marcada fuera realmente el caso de uso previsto, entonces la plantilla sería incorrecta.
Ionoclast Brigham
1
@JohnH. Dado que varios compiladores implementan -fpermissiveo similar, sí, es posible. No sé los detalles de cómo se implementa, pero el compilador debe diferir la resolución xhasta que conozca la clase base tempate 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 hecho T, 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 otro xde otro alcance son muy pequeñas!
Steve Jessop
13

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

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 en El punto de definición de la plantilla o miembro de la clase o durante una instanciación de la plantilla o miembro de la clase.

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.

Ali
fuente
11

El xestá oculto 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; }
};
chrisaycock
fuente
25
Esta respuesta no explica por qué está oculto.
jamesdlin