Buscar..


enable_if

std::enable_if es una utilidad conveniente para usar condiciones booleanas para activar SFINAE. Se define como:

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

Es decir, enable_if<true, R>::type es un alias para R , mientras que enable_if<false, T>::type está mal formado porque la especialización de enable_if no tiene un type miembro de tipo.

std::enable_if se puede usar para restringir plantillas:

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

Aquí, una llamada a negate(1) fallaría debido a la ambigüedad. Pero la segunda sobrecarga no está diseñada para usarse con tipos integrales, por lo que podemos agregar:

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

Ahora, la instanciación de negate<int> resultaría en una falla de sustitución ya que el valor !std::is_arithmetic<int>::value es false . Debido a SFINAE, esto no es un error grave, este candidato simplemente se elimina del conjunto de sobrecarga. Como resultado, negate(1) solo tiene un único candidato viable, que luego se llama.

Cuando usarlo

Vale la pena tener en cuenta que std::enable_if es un ayudante además de SFINAE, pero no es lo que hace que SFINAE funcione en primer lugar. Consideremos estas dos alternativas para implementar una funcionalidad similar a std::size , es decir, un size(arg) conjunto de sobrecarga size(arg) que produce el tamaño de un contenedor o matriz:

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

Suponiendo que is_sizeable esté escrito correctamente, estas dos declaraciones deberían ser exactamente equivalentes con respecto a SFINAE. ¿Cuál es la más fácil de escribir y la más fácil de revisar y entender de un vistazo?

Ahora, consideremos cómo podríamos implementar ayudantes aritméticos que eviten el desbordamiento de enteros con signo en favor de un comportamiento envolvente o modular. Lo que quiere decir que, por ejemplo, incr(i, 3) sería lo mismo que i += 3 excepto por el hecho de que el resultado siempre se definirá incluso si i es un int con valor INT_MAX . Estas son dos alternativas posibles:

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

Una vez más, ¿cuál es el más fácil de escribir y cuál es el más fácil de revisar y entender de un vistazo?

Una fortaleza de std::enable_if es cómo juega con la refactorización y el diseño de API. Si is_sizeable<Cont>::value pretende reflejar si cont.size() es válido, entonces usar la expresión como aparece para size1 puede ser más conciso, aunque eso dependerá de si is_sizeable se usaría en varios lugares o no. . Contraste que con std::is_signed que refleja su intención mucho más claramente que cuando su implementación se filtra en la declaración de incr1 .

void_t

C ++ 11

void_t es una meta-función que mapea cualquier tipo (número de) al tipo void . El propósito principal de void_t es facilitar la escritura de rasgos de tipo.

std::void_t será parte de C ++ 17, pero hasta entonces, es muy sencillo de implementar:

template <class...> using void_t = void;

Algunos compiladores requieren una implementación ligeramente diferente:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

La aplicación principal de void_t es escribir rasgos de tipo que comprueban la validez de una declaración. Por ejemplo, verifiquemos si un tipo tiene una función miembro foo() que no tome argumentos:

template <class T, class=void>
struct has_foo : std::false_type {};

template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

¿Como funciona esto? Cuando intento crear has_foo<T>::value instancia de has_foo<T>::value , el compilador intentará buscar la mejor especialización para has_foo<T, void> . Tenemos dos opciones: la principal y la secundaria, que implica tener que instanciar esa expresión subyacente:

  • Si T tiene una función miembro foo() , entonces cualquier tipo que devuelve se convierte a void , y la especialización se prefiere al primario basado en las reglas de ordenación parcial de la plantilla. Entonces has_foo<T>::value será true
  • Si T no tiene una función miembro de este tipo (o requiere más de un argumento), entonces la sustitución falla para la especialización y solo tenemos la plantilla principal para el respaldo. Por lo tanto, has_foo<T>::value es false .

Un caso más simple:

template<class T, class=void>
struct can_reference : std::false_type {};

template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};

esto no usa std::declval o decltype .

Puedes notar un patrón común de un argumento vacío. Podemos factorizar esto:

struct details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:
    std::false_type
  {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type
  {};
};

template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

que oculta el uso de std::void_t y hace que can_apply actúe como un indicador si el tipo suministrado como primer argumento de plantilla está bien formado después de sustituir los otros tipos en él. Los ejemplos anteriores pueden ahora reescribirse usando can_apply como:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

y:

template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());

template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >;    // Is T.foo() well formed for T?

Lo que parece más sencillo que las versiones originales.

Hay post-C ++ 17 propuestas para std rasgos similares a can_apply .

La utilidad de void_t fue descubierta por Walter Brown. Dio una maravillosa presentación al respecto en CppCon 2016.

arrastrando decltype en plantillas de funciones

C ++ 11

Una de las funciones de restricción es usar el tipo de decltype final para especificar el tipo de retorno:

namespace details {
   using std::to_string;

   // this one is constrained on being able to call to_string(T)
   template <class T>
   auto convert_to_string(T const& val, int )
       -> decltype(to_string(val))
   {
       return to_string(val);
   }

   // this one is unconstrained, but less preferred due to the ellipsis argument
   template <class T>
   std::string convert_to_string(T const& val, ... )
   {
       std::ostringstream oss;
       oss << val;
       return oss.str();
   }
}

template <class T>
std::string convert_to_string(T const& val)
{
    return details::convert_to_string(val, 0);
}

Si llamo a convert_to_string() con un argumento con el que puedo invocar a to_string() , entonces tengo dos funciones viables para details::convert_to_string() . Se prefiere el primero ya que la conversión de 0 a int es una mejor secuencia de conversión implícita que la conversión de 0 a ...

Si llamo a convert_to_string() con un argumento desde el cual no puedo invocar a to_string() , entonces la primera instanciación de la plantilla de función conduce a una falla de sustitución (no hay decltype(to_string(val)) ). Como resultado, ese candidato se elimina del conjunto de sobrecarga. La segunda plantilla de función no está restringida, por lo que está seleccionada y en su lugar, pasamos por el operator<<(std::ostream&, T) . Si ese no está definido, entonces tenemos un error de compilación con una pila de plantillas en la línea oss << val .

Que es el SFINAE

SFINAE significa S ubstitution F ailure I s N ot A n E rror. El código mal formado que resulta de la sustitución de tipos (o valores) para instanciar una plantilla de función o una plantilla de clase no es un error de compilación, solo se trata como un error de deducción.

Los fallos de deducción en las plantillas de función de creación de instancias o las especializaciones de plantillas de clase eliminan a ese candidato del conjunto de consideración, como si ese candidato fallido no existiera para empezar.

template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }

int vals[10];
begin(vals); // OK. The first function template substitution fails because
             // vals.begin() is ill-formed. This is not an error! That function
             // is just removed from consideration as a viable overload candidate,
             // leaving us with the array overload. 

Solo las fallas de sustitución en el contexto inmediato se consideran fallas de deducción, todas las demás se consideran errores graves.

template <class T>
void add_one(T& val) { val += 1; }

int i = 4;
add_one(i); // ok

std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
              // failure is NOT in the immediate context of substituting T

enable_if_all / enable_if_any

C ++ 11

Ejemplo motivacional


Cuando tiene un paquete de plantillas variadas en la lista de parámetros de la plantilla, como en el siguiente fragmento de código:

template<typename ...Args> void func(Args &&...args) { //... };

La biblioteca estándar (antes de C ++ 17) no ofrece una forma directa de escribir enable_if para imponer restricciones SFINAE en todos los parámetros en Args o cualquiera de los parámetros en Args . C ++ 17 ofrece std::conjunction y std::disjunction que resuelven este problema. Por ejemplo:

/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

Si no tiene C ++ 17 disponible, existen varias soluciones para lograrlo. Una de ellas es usar una clase de caso base y especializaciones parciales , como se demuestra en las respuestas de esta pregunta .

Alternativamente, también se puede implementar a mano el comportamiento de std::conjunction y std::disjunction de una manera bastante directa. En el siguiente ejemplo, demostraré las implementaciones y las combinaré con std::enable_if para producir dos alias: enable_if_all y enable_if_any , que hacen exactamente lo que se supone que deben hacer semánticamente. Esto puede proporcionar una solución más escalable.


Implementación de enable_if_all y enable_if_any


Primero seq_and std::conjunction y std::disjunction usando seq_and y seq_or respectivamente:

/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;

/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};

template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>: 
  conditional_t<B1,std::true_type,seq_or<Bs...>> {};

template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
  conditional_t<B1,seq_and<Bs...>,std::false_type> {};  

Entonces la implementación es bastante sencilla:

template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;

template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;

Eventualmente algunos ayudantes:

template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;

template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;

Uso


El uso también es sencillo:

    /// SFINAE constraints on all of the parameters in Args.
    template<typename ...Args,
             enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

    /// SFINAE constraints on any of the parameters in Args.
    template<typename ...Args,
             enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

is_detected

Para generalizar la creación de type_trait: en base a SFINAE hay rasgos experimentales detected_or o detected_t , is_detected .

Con los parámetros de la plantilla typename Default , template <typename...> Op y typename ... Args :

  • is_detected : alias de std::true_type o std::false_type dependiendo de la validez de Op<Args...>
  • detected_t : alias de Op<Args...> o nonesuch dependiendo de validez de Op<Args...> .
  • detected_or : alias de una estructura con value_t que is_detected , y type que es Op<Args...> o Default dependiendo de la validez de Op<Args...>

que se puede implementar utilizando std::void_t para SFINAE de la siguiente manera:

C ++ 17
namespace detail {
    template <class Default, class AlwaysVoid,
              template<class...> class Op, class... Args>
    struct detector
    {
        using value_t = std::false_type;
        using type = Default;
    };

    template <class Default, template<class...> class Op, class... Args>
    struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
    {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

} // namespace detail

// special type to indicate detection failure
struct nonesuch {
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected =
    typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;

Los rasgos para detectar la presencia del método se pueden implementar simplemente:

typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {
    int foo(char) const;
};

template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
              "Unexpected");

static_assert(std::is_same<void, // Default
                           detected_or<void, foo_type, C1, char>>::value,
              "Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
              "Unexpected");

Resolución de sobrecarga con un gran número de opciones.

Si necesita seleccionar entre varias opciones, habilitar solo una a través de enable_if<> puede ser bastante engorroso, ya que varias condiciones también deben ser negadas.

La ordenación entre sobrecargas se puede seleccionar en su lugar utilizando la herencia, es decir, el envío de etiquetas.

En lugar de probar lo que necesita estar bien formado, y también probar la negación de todas las demás condiciones de la versión, en lugar de eso, probamos solo lo que necesitamos, preferiblemente en un tipo de decltype en un retorno final.
Esto podría dejar varias opciones bien formadas, diferenciamos entre aquellos que usan 'etiquetas', similares a las etiquetas random_access_tag -rasgo ( random_access_tag et al). Esto funciona porque una coincidencia directa es mejor que una clase base, que es mejor que una clase base de una clase base, etc.

#include <algorithm>
#include <iterator>

namespace detail
{
    // this gives us infinite types, that inherit from each other
    template<std::size_t N>
    struct pick : pick<N-1> {};
    template<>
    struct pick<0> {};

    // the overload we want to be preferred have a higher N in pick<N>
    // this is the first helper template function
    template<typename T>
    auto stable_sort(T& t, pick<2>)
        -> decltype( t.stable_sort(), void() )
    {
        // if the container have a member stable_sort, use that
        t.stable_sort();
    }

    // this helper will be second best match
    template<typename T>
    auto stable_sort(T& t, pick<1>)
        -> decltype( t.sort(), void() )
    {
        // if the container have a member sort, but no member stable_sort
        // it's customary that the sort member is stable
        t.sort();
    }

    // this helper will be picked last
    template<typename T>
    auto stable_sort(T& t, pick<0>)
        -> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
    {
        // the container have neither a member sort, nor member stable_sort
        std::stable_sort(std::begin(t), std::end(t));
    }

}

// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
    // use an N that is higher that any used above.
    // this will pick the highest overload that is well formed.
    detail::stable_sort(t, detail::pick<10>{});
}

Existen otros métodos que se usan comúnmente para diferenciar las sobrecargas, como que la coincidencia exacta sea mejor que la conversión, que sea mejor que la elipsis.

Sin embargo, el envío de etiquetas puede extenderse a cualquier número de opciones, y es un poco más claro en la intención.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow