2010-08-04 10 views
38

¿Qué es SFINAE en C++?Explicar C++ SFINAE a un programador que no es C++

¿Puede explicarlo en palabras comprensibles para un programador que no está versado en C++? Además, ¿a qué concepto en un lenguaje como Python corresponde SFINAE?

+0

Cómo sabes plantillas? – Anycorn

+0

@aaa: Sí, lo hago. Sin embargo, no estarán familiarizadas todas las reglas relacionadas con ellos. – Jim

+0

Aunque carece de la orientación de Python, http://stackoverflow.com/questions/982808/c-sfinae-examples es casi un duplicado. No creo que realmente haya un análogo directo en Python. SFINAE se usa principalmente para la metaprogramación de plantillas, que ocurre en tiempo de compilación, pero Python en su mayoría no diferencia fuertemente entre el tiempo de compilación y el tiempo de ejecución como C++. –

Respuesta

97

Advertencia: se trata de una larga explicación realmente, pero es de esperar que realmente explica no sólo lo hace SFINAE, pero da una idea de cuándo y por qué es posible utilizarlo.

Bien, para explicar esto, probablemente necesitemos hacer una copia de seguridad y explicar un poco las plantillas. Como todos sabemos, Python usa lo que comúnmente se conoce como mecanografía de pato; por ejemplo, cuando invocas una función, puedes pasar un objeto X a esa función siempre que X proporcione todas las operaciones utilizadas por la función.

En C++, una función normal (sin plantilla) requiere que especifique el tipo de parámetro. Si ha definido una función como:

int plus1(int x) { return x + 1; } 

Puede única aplicar esa función a un int. El hecho de que utiliza x de manera que podría igual de bien aplicar a otros tipos como long o float no hace ninguna diferencia; solo se aplica a un int de todos modos.

para conseguir algo más cerca de la tipificación de pato de Python, puede crear una plantilla en su lugar:

template <class T> 
T plus1(T x) { return x + 1; } 

Ahora nuestro plus1 es mucho más parecido a lo que sería en Python - en particular, podemos invocar igualmente bien a un objeto x de cualquier tipo para el cual se define x + 1.

Ahora, tenga en cuenta, por ejemplo, que queremos escribir algunos objetos a un arroyo. Desafortunadamente, algunos de esos objetos se escriben en una secuencia usando stream << object, pero otros usan object.write(stream); en su lugar. Queremos ser capaces de manejar cualquiera sin que el usuario tenga que especificar cuál. Ahora, plantilla especialización nos permite escribir la plantilla especializada, por lo que si era uno tipo que utiliza la sintaxis object.write(stream), que podría hacer algo como:

template <class T> 
std::ostream &write_object(T object, std::ostream &os) { 
    return os << object; 
} 

template <> 
std::ostream &write_object(special_object object, std::ostream &os) { 
    return object.write(os); 
} 

Eso está bien para un tipo, y si queríamos bastante mal podríamos añadir más especializaciones para todos los tipos que no son compatibles stream << object - pero tan pronto como (por ejemplo) el usuario añade un nuevo tipo que no es compatible stream << object, las cosas se rompen de nuevo.

Lo que queremos es una forma de utilizar la primera especialización para cualquier objeto que admita stream << object;, pero la segunda para cualquier otra cosa (aunque en algún momento desearíamos agregar una tercera para los objetos que usan x.print(stream);).

Podemos usar SFINAE para tomar esa determinación. Para hacer eso, normalmente dependemos de un par de detalles extraños de C++. Una es usar el operador sizeof. sizeof determina el tamaño de un tipo o una expresión, pero lo hace por completo al compilar examinado los tipos involucrados, sin evaluar la expresión misma. Por ejemplo, si tengo algo como:

int func() { return -1; } 

puedo usar sizeof(func()). En este caso, func() devuelve un int, por lo que es equivalente a sizeof(func())sizeof(int).

El segundo elemento interesante que se utiliza con frecuencia es el hecho de que el tamaño de una matriz debe ser positivo, no cero.

Ahora, poniendo los juntos, podemos hacer algo como esto:

// stolen, more or less intact from: 
//  http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles 
template<class T> T& ref(); 
template<class T> T val(); 

template<class T> 
struct has_inserter 
{ 
    template<class U> 
    static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]); 

    template<class U> 
    static long test(...); 

    enum { value = 1 == sizeof test<T>(0) }; 
    typedef boost::integral_constant<bool, value> type; 
}; 

Aquí tenemos dos sobrecargas de test. El segundo de ellos toma una lista de argumentos variable (la ...) lo que significa que puede coincidir con cualquier tipo - pero también es la última opción del compilador hará que en la selección de una sobrecarga, por lo que va única partido si el primero lo hace no. El otro sobrecarga de test es un poco más interesante: define una función que toma un parámetro: una matriz de punteros a funciones que devuelven char, donde el tamaño de la matriz es (en esencia) sizeof(stream << object). Si stream << object no es una expresión válida, sizeof arrojará 0, lo que significa que hemos creado una matriz de tamaño cero, que no está permitido. Aquí es donde la propia SFINAE entra en escena.Intentar sustituir el tipo que no admite operator<< por U fallaría, porque produciría una matriz de tamaño cero. Pero eso no es un error, solo significa que la función se elimina del conjunto de sobrecarga. Por lo tanto, la otra función es la única que se puede usar en tal caso.

que luego se utiliza en la expresión enum abajo - se observa el valor de retorno de la sobrecarga seleccionado de test y comprueba si es igual a 1 (si lo es, significa que la función de regresar char fue seleccionado, pero por lo demás , se seleccionó la función que devuelve long).

El resultado es que habrá has_inserter<type>::valuel si podíamos usar some_ostream << object; sería compilar y 0 si no lo haría. Entonces podemos usar ese valor para controlar la especialización de plantillas para elegir la forma correcta de escribir el valor para un tipo particular.

+12

* obligatorio "desearía poder votar esto más de una vez" comentario * Gran explicación, también aprendí algo :) – Thomas

+2

Esta es una explicación completa, con ejemplos decentes también. – Carlos

+1

Hmm, la última vez que revisé, esta cosa en particular dio resultados inconsistentes entre los diferentes compiladores. Prueba de nuevo: GCC - bien, VC++ 2005 (lo siento por la versión anterior) - todo es imprimible, codepad.org - "Línea 12: error: matriz encuadernada no es una constante entera", Comeau Online - 'static_assert (is_printable :: value , "int ok") 'falla. Ojalá ** SER estudiante ** lea esta publicación, porque este es un material real para "probar el cumplimiento estándar de varias plantillas de compilación de wrt" :). No tengo idea de qué compilador confiar en este caso ... – UncleBens

10

Si tiene algunas funciones de plantilla sobrecargadas, algunos de los posibles candidatos para el uso pueden no ser compilables cuando se realiza la sustitución de plantilla, porque la cosa que se sustituye puede no tener el comportamiento correcto. Esto no se considera un error de programación, las plantillas fallidas simplemente se eliminan del conjunto disponible para ese parámetro en particular.

No tengo idea si Python tiene una función similar, y realmente no veo por qué un programador que no es C++ debería preocuparse por esta característica. Pero si desea obtener más información acerca de las plantillas, el mejor libro sobre ellas es C++ Templates: The Complete Guide.

+3

"[Realmente] no veo por qué un programador que no es C++ debería preocuparse por esta característica". <- porque estoy aprendiendo C++ ahora. – Jim

+8

@Jim Bueno, SFINAE debería estar muy abajo en tu lista de cosas que realmente necesitas saber. –

+0

Quiero saberlo. Ahora. – Jim

3

No hay nada en Python que se parezca remotamente a SFINAE. Python no tiene plantillas, y ciertamente no tiene una resolución de función basada en parámetros como ocurre cuando se resuelven las especializaciones de plantillas. La búsqueda de funciones se hace solo por nombre en Python.

+2

¿No debería ser este un comentario? – Jim

+3

si es así? Responde parte de la pregunta. – jalf

5

Python no lo ayudará en absoluto. Pero dices que ya estás familiarizado con las plantillas.

El constructo SFINAE más fundamental es el uso de enable_if. La única parte difícil es que class enable_if no encapsula SFINAE, simplemente lo expone.

template< bool enable > 
class enable_if { }; // enable_if contains nothing… 

template<> 
class enable_if<true> { // … unless argument is true… 
public: 
    typedef void type; // … in which case there is a dummy definition 
}; 

template< bool b > // if "b" is true, 
typename enable_if<b>::type function() {} //the dummy exists: success 

template< bool b > 
typename enable_if< ! b >::type function() {} // dummy does not exist: failure 
    /* But Substitution Failure Is Not An Error! 
    So, first definition is used and second, although redundant and 
    nonsensical, is quietly ignored. */ 

int main() { 
    function<true>(); 
} 

En SFINAE, hay algún tipo de estructura que establece una condición de error (class enable_if aquí) y una serie de paralelo, las definiciones de otro modo en conflicto. Se produce un error en todas las definiciones menos una, que el compilador selecciona y utiliza sin quejarse de las demás.

Qué tipos de errores son aceptables es un detalle importante que se ha estandarizado recientemente, pero parece que no se pregunta al respecto.

+0

Tal como está, el código es simplemente un error: "' clase "enable_if " no tiene miembro "tipo" '" y "' no puede sobrecargar funciones que se distinguen por tipo de retorno solo' ". 'function' debe ser una plantilla, y la parte (potencialmente) que falla debe depender de los argumentos de la plantilla. – UncleBens

+0

@Uncle: oops, simplifiqué demasiado. ¡SFINAE no funciona en un entorno sin plantilla! – Potatoswatter

7

SFINAE es un principio un compilador C++ utiliza para filtrar algunas sobrecargas de función con plantilla durante la resolución de sobrecarga (1)

Cuando el compilador resuelve una llamada de función particular, se considera un conjunto de funciones disponibles y plantilla de función declaraciones para descubrir cuál será utilizado. Básicamente, hay dos mecanismos para hacerlo. Uno puede describirse como sintáctico. declaraciones dado:

template <class T> void f(T);     //1 
template <class T> void f(T*);    //2 
template <class T> void f(std::complex<T>); //3 

resolver f((int)1) eliminará las versiones 2 y tres, porque int no es igual a complex<T> o T* para algunos T. Del mismo modo, f(std::complex<float>(1)) eliminaría la segunda variante y f((int*)&x) eliminaría la tercera. El compilador hace esto tratando de deducir los parámetros de la plantilla de los argumentos de la función. Si la deducción falla (como en T* contra int), la sobrecarga se descarta.

La razón por la que queremos esto es obvio: es posible que deseemos hacer cosas ligeramente diferentes para diferentes tipos (por ejemplo, un valor absoluto de un complejo se calcula por x*conj(x) y produce un número real, no un número complejo, que es diferente del cómputo de carrozas).

Si ha hecho algo de programación declarativa antes, este mecanismo es similar a (Haskell):

f Complex x y = ... 
f _   = ... 

La forma en C++ toma este adicional es que la deducción puede fallar incluso cuando los tipos deducidas están bien, pero la sustitución de regreso en la otra cede un resultado "sin sentido" (más sobre esto más adelante). Por ejemplo:

template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0); 

cuando deducir f('c') (llamamos con un solo argumento, porque el segundo argumento es implícita):

  1. el compilador partidos T contra char que produce trivialmente T como char
  2. el compilador sustituye todos los T s en la declaración como char s. Esto produce void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).
  3. El tipo del segundo argumento es el puntero a la matriz int [sizeof(char)-sizeof(int)]. El tamaño de esta matriz puede ser, por ejemplo.-3 (dependiendo de su plataforma).
  4. Las matrices de longitud <= 0 no son válidas, por lo que el compilador descarta la sobrecarga. Error de sustitución no es un error, el compilador no rechazará el programa.

Al final, si queda más de una función de sobrecarga, el compilador usa la comparación de secuencias de conversión y el ordenamiento parcial de las plantillas para seleccionar una que sea la "mejor".

Existen más resultados "sin sentido" que funcionan de esta manera, se enumeran en una lista en el estándar (C++ 03). En C++ 0x, el ámbito de SFINAE se extiende a casi cualquier tipo de error.

no voy a escribir una extensa lista de errores SFINAE, pero algunos de los más populares son:

  • la selección de un tipo anidado de un tipo que no lo tenga. p.ej. typename T::type para T = int o T = A donde A es una clase sin un tipo anidado llamado type.
  • creando un tipo de matriz de tamaño no positivo. Para un ejemplo, vea this litb's answer
  • creando un puntero de miembro a un tipo que no es una clase. p.ej. int C::* para C = int

Este mecanismo no es similar a cualquier cosa en otros lenguajes de programación que conozco. Si hicieras algo similar en Haskell, usarías guardias que son más potentes, pero imposibles en C++.


1: o parciales especializaciones de plantilla cuando se habla de plantillas de clase

+0

'sizeof (char) -sizeof (int)' es una expresión de tipo 'size_t', por lo que nunca puede ser negativo; puede ser 4294967293U, por ejemplo. – musiphil

Cuestiones relacionadas