c++ - Acceso de miembros privados en sustitución de plantillas y SFINAE.
templates gcc (1)
¡Esto es muy interesante! Creo que es un error del compilador g ++ y creo que esto es lo que sucede. He intentado varias modificaciones de su código con g ++ 4.9.3 y clang 3.7.0.
Si bien existen reglas algo diferentes para la creación de instancias frente a la plantilla de clase, creo que estos son los pasos generales para la creación de una instancia de plantilla:
- El compilador lee el archivo fuente con definiciones de plantillas.
- Búsqueda de nombre (puede desencadenar ADL): es el procedimiento mediante el cual un nombre, cuando se encuentra en un programa, se asocia con la declaración que lo introdujo. ( http://en.cppreference.com/w/cpp/language/lookup )
- Especificación / deducción de los argumentos de la plantilla: para crear una instancia de una plantilla de función, se deben conocer todos los argumentos de la plantilla, pero no se debe especificar cada uno de ellos. Cuando sea posible, el compilador deducirá los argumentos de la plantilla que faltan de los argumentos de la función. ( http://en.cppreference.com/w/cpp/language/template_argument_deduction )
- Sustitución de plantilla (puede desencadenar SFINAE): cada uso de un parámetro de plantilla en la lista de parámetros de función se reemplaza con los argumentos de plantilla correspondientes. Falla de sustitución (es decir, falla al reemplazar los parámetros de plantilla con los argumentos de plantilla deducidos o proporcionados) elimina la plantilla de función del conjunto de sobrecarga. ( http://en.cppreference.com/w/cpp/language/function_template#Template_argument_substitution )
- Formación del conjunto de sobrecarga: antes de que comience la resolución de sobrecarga, las funciones seleccionadas por búsqueda de nombre y deducción de argumentos de plantilla se combinan para formar el conjunto de funciones candidatas. ( http://en.cppreference.com/w/cpp/language/overload_resolution#Details )
- Resolución de sobrecarga: en general, la función candidata cuyos parámetros coinciden con los argumentos más cercanos es la que se llama. ( http://en.cppreference.com/w/cpp/language/overload_resolution )
- Creación de instancias de plantilla: los argumentos de la plantilla deben determinarse para que el compilador pueda generar una función real (o clase, a partir de una plantilla de clase). ( http://en.cppreference.com/w/cpp/language/function_template#Function_template_instantiation )
- El compilador genera código.
Guardaré estos puntos como pautas y referencias para más adelante. Además, me referiré a la evaluación de plantillas de los pasos 1-6. Si encuentra algo incorrecto en la lista anterior, siéntase libre de cambiarlo o comentarlo para que pueda hacer los cambios.
En el siguiente ejemplo:
class A {};
template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };
template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };
int main()
{ test<A> a; }
Salida de ambos compiladores:
Using None
Este ejemplo compila bien tanto en g ++ como en clang, porque cuando el compilador completa el proceso de evaluación de todas las plantillas, solo elegirá crear una instancia de la primera plantilla para que sea la mejor coincidencia con los argumentos de la plantilla utilizados para crear el objeto en main ( ). Además, el proceso de sustitución de plantillas falla cuando el compilador no puede deducir T :: a (SFINAE). Además, debido a la discrepancia de argumentos, la especialización no se incluirá en el conjunto de sobrecarga y no se creará una instancia.
Deberíamos agregar el segundo argumento de la plantilla, como este:
test<A , decltype(A::a)> a;
El código no se compilará y ambos compiladores se quejarían de:
error: no member named ''a'' in ''A''
En el siguiente ejemplo, sin embargo, las cosas comienzan a ser extrañas:
class A { int a; };
template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };
template<typename T>
struct test<T, decltype(T::a)>
{ test(){std::cout<< "Using T::a" <<std::endl;} };
int main()
{ test<A> a; }
Salida de clang:
Using None
Salida de g ++:
error: ‘int A::a’ is private
Para empezar, creo que esto habría sido una buena advertencia. Pero ¿por qué un error? La plantilla ni siquiera se instanciará. Teniendo en cuenta el ejemplo anterior, y el hecho de que los punteros a los miembros son valores constantes conocidos en el momento de la compilación, parece que cuando clang completa la etapa de evaluación de la plantilla, con el SFINAE en la sustitución de la plantilla, crea una instancia de la primera plantilla y la ignora. La especialización. Pero cuando g ++ pasa por el proceso de sustitución y busca la variable T :: a, ve que es un miembro privado y, en lugar de decir SFINAE, le indica el error anterior. Creo que aquí es donde está el error, considerando este informe de error: this
Ahora, la parte curiosa está en el siguiente ejemplo, que utiliza la función de miembro privado:
class A{ void a() {}; };
template<typename, typename = void>
struct test
{ test(){std::cout<< "Using None" <<std::endl;} };
template<typename T>
struct test<T,decltype(&T::a)>
{ test(){std::cout<< "Using A::a" <<std::endl;} };
int main()
{ test<A> a; }
Salida por ambos compiladores:
Using None
Si la explicación anterior es verdadera, ¿por qué g ++ no emite un error cuando se utiliza la función de miembro privado? Nuevamente, esto es solo una suposición basada en las salidas, pero creo que este bit realmente funciona como debería. En pocas palabras, SFINAE entra en acción, la especialización se descarta del conjunto de sobrecarga y solo se crea una instancia de la primera plantilla. Tal vez haya más de lo que parece, pero si especificamos explícitamente el segundo argumento de la plantilla, ambos compiladores generarán el mismo error.
int main()
{ test<A , decltype(&A::a)> b; }
Salida por ambos compiladores:
error: ‘void A::a()’ is private
De todos modos, este es el código final que he estado usando. Para demostrar los resultados, he hecho pública la clase. Como evento interesante, agregué un nullptr para apuntar directamente a la función miembro. El tipo de decltype (((T *) nullptr) -> f ()) es inválido , y del siguiente ejemplo, a y c son invocados por la especialización en lugar de la primera plantilla. La razón es que la segunda plantilla es más especializada que la primera y, por lo tanto, es la mejor combinación para ambas (matar dos pájaros de un tiro) (Reglas formales de pedido de la plantilla: https://stackoverflow.com/a/9993549/2754510 ). El tipo de decltype (& T :: f) es M4GolfFvvE (posible traducción: Men 4 Golf Fear Elk muy vicioso), que gracias a boost :: typeindex :: type_id_with_cvr, se divide en void (Golf :: *) () .
#include <iostream>
#include <boost/type_index.hpp>
class Golf
{
public:
int v;
void f()
{};
};
template<typename T>
using var_t = decltype(T::v);
template<typename T>
using func_t = decltype(&T::f);
//using func_t = decltype(((T*)nullptr)->f()); //void
template<typename, typename = void>
struct test
{
test(){std::cout<< "Using None" <<std::endl;}
};
template<typename T>
struct test<T,var_t<T> >
{
test(){std::cout<< "Using Golf::v" <<std::endl;}
};
template<typename T>
struct test<T,func_t<T> >
{
test(){std::cout<< "Using Golf::f" <<std::endl;}
};
int main()
{
test<Golf> a;
test<Golf,var_t<Golf> > b;
test<Golf,func_t<Golf> > c;
using boost::typeindex::type_id_with_cvr;
std::cout<< typeid(func_t<Golf>).name() << " -> " << type_id_with_cvr<func_t<Golf>>().pretty_name() <<std::endl;
}
Salida de ambos compiladores (func_t = decltype (& T :: f)):
Using None
Using Golf::v
Using Golf::f
M4GolfFvvE -> void (Golf::*)()
Salida de ambos compiladores (func_t = decltype (((T *) nullptr) -> f ())):
Using Golf::f
Using Golf::v
Using Golf::f
v -> void
class A { int a; };
template<typename, typename = void>
class test {};
template<typename T>
class test<T,decltype(T::a)> {};
int main() { test<A> a; }
El código anterior compila sin errores en la clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
, pero no puede compilar en g++-5 (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904
y g++-6 (Ubuntu 6.2.0-3ubuntu11~16.04) 6.2.0 20160901
con errores como este:
main.cpp: In function ‘int main()’:
main.cpp:9:22: error: ‘int A::a’ is private within this context
int main() { test<A> a; }
^
main.cpp:1:15: note: declared private here
class A { int a; };
En ambos casos compilé con -std=c++11
, pero el efecto es el mismo para -std=c++14
y -std=c++1z
.
¿Qué compilador es correcto aquí? Asumí que, al menos desde C ++ 11, el acceso a miembros privados durante la sustitución de la plantilla debería activar SFINAE, lo que implica que el clang es correcto y que gcc no lo es. ¿Hay alguna regla adicional que desconozco?
Para referencia, estoy pensando en la nota en §14.8.2 / 8 del borrador estándar actual N4606:
Si una sustitución produce un tipo o expresión no válida, la deducción de tipo falla. Un tipo o expresión inválida es aquella que no se formará correctamente, con un diagnóstico requerido, si se escribe usando los argumentos sustituidos. [Nota: Si no se requiere ningún diagnóstico, el programa aún está mal formado. La verificación de acceso se realiza como parte del proceso de sustitución. - nota final]
El uso de una función miembro y un puntero de función miembro en su lugar es aceptado por ambos compiladores:
class A { void a() {}; };
template<typename, typename = void>
class test {};
template<typename T>
class test<T,decltype(&T::a)> {};
int main() { test<A> a; }