c++ - ¿Cómo funciona la implementación de std:: is_function de Eric Niebler?
c++11 templates (1)
La idea general
En lugar de enumerar todos los tipos de funciones válidos, como la
std::is_function
, esta implementación enumera todos los tipos que
no
son funciones y luego solo se resuelve como
true
si ninguno de ellos coincide.
La lista de tipos sin función consiste en (de abajo hacia arriba):
- Clases y uniones (incluidos los tipos abstractos)
-
Cualquier cosa que pueda devolverse desde una función (incluidos los tipos de referencia y
void
) - Tipos de matriz
Un tipo que no coincide con ninguno de esos tipos sin función es un tipo de función.
Tenga en cuenta que
std::is_function
considera explícitamente que los tipos invocables como lambdas o clases con un operador de llamada a función
no
son funciones.
is_function_impl_
Proporcionamos una sobrecarga de la función
is_function_impl
para cada uno de los posibles tipos sin función.
Las declaraciones de funciones pueden ser un poco difíciles de analizar, así que vamos a desglosarlas para el ejemplo del caso de
clases y uniones
:
template<typename T, typename = int T::*>
char(&is_function_impl_(priority_tag<3>))[4];
Esta línea declara una plantilla de función
is_function_impl_
que toma un solo argumento de tipo
priority_tag<3>
y devuelve una referencia a una matriz de 4 caracteres.
Como es habitual desde los antiguos tiempos de C, la sintaxis de la declaración se complica terriblemente por la presencia de tipos de matriz.
Esta plantilla de función toma dos argumentos de plantilla.
El primero es solo una
T
sin restricciones, pero el segundo es un puntero a un miembro de
T
de tipo
int
.
La parte
int
aquí realmente no importa, es decir.
esto incluso funcionará para
T
s que no tienen miembros de tipo
int
.
Sin embargo, lo que hace es que dará como resultado un error de sintaxis para
T
que no son de tipo clase o unión.
Para esos otros tipos, intentar crear una instancia de la plantilla de función resultará en una falla de sustitución.
Se utilizan trucos similares para las sobrecargas
priority_tag<2>
y
priority_tag<1>
, que utilizan sus segundos argumentos de plantilla para formar expresiones que solo compilan para
T
como tipos de retorno de función válidos o tipos de matriz, respectivamente.
Solo la sobrecarga
priority_tag<0>
no tiene un segundo parámetro de plantilla tan restrictivo y, por lo tanto, puede instanciarse con cualquier
T
En total, declaramos cuatro sobrecargas diferentes para
is_function_impl_
, que difieren en su argumento de entrada y tipo de retorno.
Cada uno de ellos toma un tipo de
priority_tag
diferente como argumento y devuelve una referencia a una matriz de caracteres de diferente tamaño único.
is_function
etiquetas en
is_function
Ahora, al crear instancias de
is_function
,
is_function_impl
instancias de
is_function_impl
con
T
Tenga en cuenta que dado que proporcionamos cuatro sobrecargas diferentes para esta función, la resolución de sobrecarga debe tener lugar aquí.
Y dado que todas estas sobrecargas son
plantillas de
funciones, eso significa que
SFINAE
tiene la oportunidad de
SFINAE
.
Entonces, para las funciones (y solo las funciones), todas las sobrecargas fallarán, excepto la más general con
priority_tag<0>
.
Entonces, ¿por qué la instanciación no siempre resuelve esa sobrecarga, si es la más general?
Debido a los argumentos de entrada de nuestras funciones sobrecargadas.
Tenga en cuenta que
priority_tag
se construye de tal manera que
priority_tag<N+1>
hereda públicamente de
priority_tag<N>
.
Ahora, dado que
is_function_impl
se invoca aquí con
priority_tag<3>
, esa sobrecarga es una
mejor coincidencia
que las otras para la resolución de sobrecarga, por lo que se intentará primero.
Solo si eso falla debido a un error de sustitución, se intenta la siguiente mejor coincidencia, que es la sobrecarga
priority_tag<2>
.
Continuamos de esta manera hasta que encontremos una sobrecarga que pueda ser instanciada o lleguemos a
priority_tag<0>
, que no está restringida y siempre funcionará.
Dado que todos los tipos sin función están cubiertos por las sobrecargas de prio más altas, esto solo puede suceder para los tipos de función.
Evaluando el resultado
Ahora inspeccionamos el tamaño del tipo devuelto por la llamada a
is_function_impl_
para evaluar el resultado.
Recuerde que cada sobrecarga devuelve una referencia a una matriz de caracteres de diferente tamaño.
Por lo tanto, podemos usar
sizeof
para verificar qué sobrecarga se seleccionó y solo establecer el resultado en
true
si alcanzamos la sobrecarga
priority_tag<0>
.
Errores conocidos
Johannes Schaub encontró un error en la implementación. Una matriz de tipo de clase incompleta se clasificará incorrectamente como una función. Esto se debe a que el mecanismo de detección actual para los tipos de matriz no funciona con tipos incompletos.
La semana pasada, Eric Niebler
tweeted
una implementación muy compacta para la clase de rasgos
std::is_function
:
#include <type_traits>
template<int I> struct priority_tag : priority_tag<I - 1> {};
template<> struct priority_tag<0> {};
// Function types here:
template<typename T>
char(&is_function_impl_(priority_tag<0>))[1];
// Array types here:
template<typename T, typename = decltype((*(T*)0)[0])>
char(&is_function_impl_(priority_tag<1>))[2];
// Anything that can be returned from a function here (including
// void and reference types):
template<typename T, typename = T(*)()>
char(&is_function_impl_(priority_tag<2>))[3];
// Classes and unions (including abstract types) here:
template<typename T, typename = int T::*>
char(&is_function_impl_(priority_tag<3>))[4];
template <typename T>
struct is_function
: std::integral_constant<bool, sizeof(is_function_impl_<T>(priority_tag<3>{})) == 1>
{};
pero como funciona?