Enfoques para funcionar SFINAE en C ++

40

Estoy usando la función SFINAE en gran medida en un proyecto y no estoy seguro de si hay alguna diferencia entre los siguientes dos enfoques (aparte del estilo):

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

El resultado del programa es el esperado:

method 1
method 2
Done...

He visto que el método 2 se usa con más frecuencia en stackoverflow, pero prefiero el método 1.

¿Hay alguna circunstancia cuando estos dos enfoques difieren?

keith
fuente
¿Cómo ejecutas este programa? No se compilará para mí.
alter igel
@alter igel necesitará un compilador C ++ 17. Utilicé MSVC 2019 para probar este ejemplo, pero principalmente trabajo con Clang.
keith
Relacionado: why-should-i-evitar-stdenable-if-in-function-signatures y C ++ 20 también presenta nuevas formas con el concepto :-)
Jarod42
@ Jarod42 Los conceptos son una de las cosas más necesarias para mí desde C ++ 20.
Val dice Reinstate Monica

Respuestas:

35

He visto que el método 2 se usa con más frecuencia en stackoverflow, pero prefiero el método 1.

Sugerencia: prefiera el método 2.

Ambos métodos funcionan con funciones individuales. El problema surge cuando tiene más de una función, con la misma firma, y ​​desea habilitar solo una función del conjunto.

Supongamos que desea habilitar foo(), versión 1, cuando bar<T>()(pretenda que es una constexprfunción) es true, y foo(), versión 2, cuándo bar<T>()es false.

Con

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

aparece un error de compilación porque tiene una ambigüedad: dos foo()funciones con la misma firma (un parámetro de plantilla predeterminado no cambia la firma).

Pero la siguiente solución

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

funciona, porque SFINAE modifica la firma de las funciones.

Observación no relacionada: también hay un tercer método: habilitar / deshabilitar el tipo de retorno (excepto para los constructores de clase / estructura, obviamente)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

Como método 2, el método 3 es compatible con la selección de funciones alternativas con la misma firma.

max66
fuente
1
Gracias por la gran explicación, preferiré los métodos 2 y 3 de ahora en adelante :-)
keith
"un parámetro de plantilla predeterminado no cambia la firma" : ¿en qué se diferencia esto en su segunda variante, que también utiliza parámetros de plantilla predeterminados?
Eric
1
@Eric: no es fácil de decir ... Supongo que la otra respuesta explica esto mejor ... Si SFINAE habilita / deshabilita el argumento de plantilla predeterminado, la foo()función permanece disponible cuando lo llama con un segundo parámetro de plantilla explícito (la foo<double, double>();llamada). Y si permanece disponible, existe una ambigüedad con la otra versión. Con el método 2, SFINAE habilita / deshabilita el segundo argumento, no el parámetro predeterminado. Por lo tanto, no puede llamarlo explicando el parámetro porque hay una falla de sustitución que no permite un segundo parámetro. Así que la versión no está disponible, por lo que no hay ambigüedad
max66
3
El método 3 tiene la ventaja adicional de que generalmente no se filtra en el nombre del símbolo. La variante a auto foo() -> std::enable_if_t<...>menudo es útil para evitar ocultar la firma de la función y permitir el uso de los argumentos de la función.
Deduplicador
@ max66: entonces, ¿el punto clave es que la falla de sustitución en un parámetro de plantilla predeterminado no es un error si se proporciona el parámetro y no se necesita ningún valor predeterminado?
Eric
21

Además de la respuesta de max66 , otra razón para preferir el método 2 es que con el método 1, puede (accidentalmente) pasar un parámetro de tipo explícito como el segundo argumento de plantilla y derrotar completamente el mecanismo SFINAE. Esto podría suceder como un error tipográfico, copiar / pegar, o como un descuido en un mecanismo de plantilla más grande.

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Demostración en vivo aquí

alter igel
fuente
Buen punto. El mecanismo puede ser secuestrado.
max66