Pregunta

¿Qué es Sfinae en C ++?

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

¿Fue útil?

Solución

Advertencia: este es un De Verdad Explicación larga, pero espero que realmente explique no solo lo que hace Sfinae, sino que da una idea de cuándo y por qué podría usarlo.

De acuerdo, 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 tipificación de pato; por ejemplo, cuando invoca una función, puede 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 definió una función como:

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

Puedes solamente Aplicar esa función a un int. El hecho de que usa x de manera que pudo A la misma manera, aplique a otros tipos como long o float No hace ninguna diferencia: solo se aplica a un int de todos modos.

Para acercar algo a la escritura 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 como lo sería en Python; en particular, podemos invocarlo igualmente bien a un objeto x de cualquier tipo para el cual x + 1 se define.

Ahora, considere, por ejemplo, que queremos escribir algunos objetos en una transmisión. Desafortunadamente, algunos de esos objetos se escriben en una transmisión usando stream << object, pero otros usan object.write(stream); en cambio. Queremos poder manejar ninguno de los dos sin que el usuario tenga que especificar cuál. Ahora, la especialización de plantillas nos permite escribir la plantilla especializada, por lo que si era una escriba que usó el object.write(stream) Sintaxis, podríamos 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 quisiéramos lo suficientemente mal, podríamos agregar más especializaciones para todos los tipos que no admiten stream << object - pero tan pronto como (por ejemplo) el usuario agrega un nuevo tipo que no admite stream << object, las cosas se rompen de nuevo.

Lo que queremos es una forma de usar la primera especialización para cualquier objeto que admita stream << object;, pero el segundo para cualquier otra cosa (aunque en algún momento quise agregar un tercero para los objetos que usan x.print(stream); en cambio).

Podemos usar SFINAE para hacer esa determinación. Para hacer eso, generalmente confiamos en un par de otros detalles de Oddball de C ++. Uno es usar el sizeof operador. sizeof determina el tamaño de un tipo o una expresión, pero lo hace completamente en el tiempo de compilación mirando el tipos involucrado, sin evaluar la expresión en sí. Por ejemplo, si tengo algo como:

int func() { return -1; }

Puedo usar sizeof(func()). En este caso, func() Devuelve un int, asi que sizeof(func()) es equivalente a sizeof(int).

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

Ahora, uniendolos, 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 estos toma una lista de argumentos variables (la ...) lo que significa que puede coincidir con cualquier tipo, pero también es la última opción que el compilador hará al seleccionar una sobrecarga, por lo que solamente coincide si el primero lo hace no. La otra sobrecarga de test es un poco más interesante: define una función que toma un parámetro: una variedad de punteros a las funciones que regresan char, donde está el tamaño de la matriz (en esencia) sizeof(stream << object). Si stream << object no es una expresión válida, la sizeof Reducirá 0, lo que significa que hemos creado una matriz de tamaño cero, lo 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 puede usarse en tal caso.

Que luego se usa en el enum Expresión a continuación: analiza el valor de retorno de la sobrecarga seleccionada de test y verifica si es igual a 1 (si es así, significa que la función que regresa char fue seleccionado, pero por lo demás, la función que regresa long fue seleccionado).

El resultado es que has_inserter<type>::value estarán l Si pudiéramos usar some_ostream << object; compilaría y 0 Si no lo haría. Luego podemos usar ese valor para controlar la especialización de la plantilla para elegir la forma correcta de escribir el valor para un tipo particular.

Otros consejos

Si tiene algunas funciones de plantilla sobrecargadas, algunos de los posibles candidatos para su uso pueden no ser compilables cuando se realiza la sustitución de la 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 de si Python tiene una característica similar, y realmente no veo por qué un programador no ++ debería preocuparse por esta característica. Pero si quieres aprender más sobre las plantillas, el mejor libro sobre ellas es Plantillas C ++: la guía completa.

SFINAE es un principio que utiliza un compilador C ++ para filtrar algunas sobrecargas de funciones plantadas durante la resolución de sobrecarga (1)

Cuando el compilador resuelve una llamada de función en particular, considera un conjunto de declaraciones de plantilla de función y función disponibles para averiguar cuál se utilizará. Básicamente, hay dos mecanismos para hacerlo. Uno puede describirse como sintáctico. Declaraciones dadas:

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

resolución f((int)1) eliminará las versiones 2 y tres, porque int no es igual a complex<T> o T* para algunos T. Similarmente, f(std::complex<float>(1)) eliminaría la segunda variante y f((int*)&x) eliminaría el tercero. El compilador hace esto tratando de deducir los parámetros de la plantilla de los argumentos de 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álculo para flotadores).

Si ha realizado una programación declarativa antes, este mecanismo es similar a (Haskell):

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

La forma en que C ++ lleva esto más allá es que la deducción puede fallar incluso cuando los tipos deducidos están bien, pero la sustitución de retroceso en el otro produce algún resultado "sin sentido" (más sobre eso más adelante). Por ejemplo:

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

Al deducir f('c') (Llamamos con un solo argumento, porque el segundo argumento está implícito):

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

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

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

No escribiré una extensa lista de errores de SFINAE, pero algunos de los más populares son:

  • Seleccionar un tipo anidado de un tipo que no lo tiene. p.ej. typename T::type por T = int o T = A dónde A es una clase sin un tipo anidado llamado type.
  • Creación de un tipo de matriz de tamaño no positivo. Para un ejemplo, ver la respuesta de este litb
  • Crear un puntero de miembro a un tipo que no sea una clase. p.ej. int C::* por C = int

Este mecanismo no es similar a nada en otros lenguajes de programación que conozco. Si hiciera algo similar en Haskell, usaría guardias que son más poderosos, pero imposibles en C ++.


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

Python no te ayudará en absoluto. Pero usted dice que ya está básicamente familiarizado con las plantillas.

La construcción SFINAE más fundamental es el uso de enable_if. La única parte difícil es que class enable_if no es encapsular 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 alguna estructura que establece una condición de error (class enable_if aquí) y una serie de definiciones paralelas y contradictorias. Se produce algún error en todas las definiciones menos una, que el compilador elige y usa sin quejarse de los demás.

Qué tipos de errores son aceptables es un detalle importante que se ha estandarizado recientemente, pero parece que no está preguntando por eso.

No hay nada en Python que se parezca remotamente a Sfinae. Python no tiene plantillas, y ciertamente no hay resolución de funciones basada en parámetros como se produce al resolver especializaciones de plantillas. La búsqueda de funciones se realiza puramente por su nombre en Python.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top