¿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?
¿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?
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>::value
l
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.
* obligatorio "desearía poder votar esto más de una vez" comentario * Gran explicación, también aprendí algo :) – Thomas
Esta es una explicación completa, con ejemplos decentes también. – Carlos
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
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.
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.
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.
Tal como está, el código es simplemente un error: "' clase "enable_if
@Uncle: oops, simplifiqué demasiado. ¡SFINAE no funciona en un entorno sin plantilla! – Potatoswatter
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):
T
contra char
que produce trivialmente T
como char
T
s en la declaración como char
s. Esto produce void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
. El tamaño de esta matriz puede ser, por ejemplo.-3 (dependiendo de su plataforma).<= 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:
typename T::type
para T = int
o T = A
donde A
es una clase sin un tipo anidado llamado type
.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
'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
Cómo sabes plantillas? – Anycorn
@aaa: Sí, lo hago. Sin embargo, no estarán familiarizadas todas las reglas relacionadas con ellos. – Jim
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++. –