¿Por qué debería evitar std :: enable_if en las firmas de funciones

165

Scott Meyers publicó el contenido y el estado de su próximo libro EC ++ 11. Escribió que un elemento del libro podría ser "Evitar std::enable_ifen firmas de funciones" .

std::enable_if puede usarse como argumento de función, como tipo de retorno o como plantilla de clase o parámetro de plantilla de función para eliminar condicionalmente funciones o clases de la resolución de sobrecarga.

En esta pregunta se muestran las tres soluciones.

Como parámetro de función:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Como parámetro de plantilla:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Como tipo de retorno:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • ¿Qué solución debería preferirse y por qué debería evitar otras?
  • ¿En qué casos "Evitar std::enable_ifen firmas de funciones" se refiere al uso como tipo de retorno (que no es parte de la firma de función normal sino de las especializaciones de plantilla)?
  • ¿Hay alguna diferencia para las plantillas de funciones miembro y no miembro?
Hansmaad
fuente
Porque la sobrecarga es igual de buena, por lo general. En todo caso, delegue a una implementación que use plantillas de clase (especializadas).
sehe
Las funciones miembro difieren en que el conjunto de sobrecarga incluye sobrecargas declaradas después de la sobrecarga actual. Esto es particularmente importante cuando se realiza el tipo de retorno diferido de variables (donde el tipo de retorno debe inferirse de otra sobrecarga)
sehe
1
Bueno, simplemente subjetivamente, tengo que decir que, aunque a menudo soy bastante útil, no me gusta std::enable_ifsaturar mis firmas de funciones (especialmente la nullptrversión fea del argumento de función adicional ) porque siempre parece lo que es, un truco extraño (por algo static ifpoderoso) hacer mucho más hermoso y limpio) usando la plantilla black-magic para explotar una función de lenguaje interesante. Es por eso que prefiero el envío de etiquetas siempre que sea posible (bueno, todavía tiene argumentos extraños adicionales, pero no en la interfaz pública y también mucho menos feo y críptico ).
Christian Rau
2
Quiero preguntar qué hace =0en typename std::enable_if<std::is_same<U, int>::value, int>::type = 0realidad? No pude encontrar los recursos correctos para entenderlo. Sé la primera parte antes =0tiene un tipo de miembro intsi Uy intes el mismo. ¡Muchas gracias!
astroboylrx
44
@astroboylrx Divertido, solo iba a poner un comentario en el que se notara esto. Básicamente, que = 0 indica que este es un parámetro de plantilla predeterminado que no es de tipo . Se hace de esta manera porque los parámetros de plantilla de tipo predeterminados no son parte de la firma, por lo que no puede sobrecargarlos.
Nir Friedman

Respuestas:

107

Pon el truco en los parámetros de la plantilla .

El enable_ifenfoque del parámetro on template tiene al menos dos ventajas sobre los demás:

  • legibilidad : el uso enable_if y los tipos return / argumento no se fusionan en una parte desordenada de desambigadores de nombres de tipos y accesos de tipos anidados; a pesar de que el desorden del desambigador y el tipo anidado se puede mitigar con plantillas de alias, eso aún fusionaría dos cosas no relacionadas. Enable_if use está relacionado con los parámetros de la plantilla, no con los tipos de retorno. Tenerlos en los parámetros de la plantilla significa que están más cerca de lo que importa;

  • Aplicabilidad universal : los constructores no tienen tipos de retorno y algunos operadores no pueden tener argumentos adicionales, por lo que ninguna de las otras dos opciones se puede aplicar en todas partes. Poner enable_if en un parámetro de plantilla funciona en todas partes ya que solo puedes usar SFINAE en plantillas de todos modos.

Para mí, el aspecto de legibilidad es el gran factor de motivación en esta elección.

R. Martinho Fernandes
fuente
44
El uso de la FUNCTION_REQUIRESmacro aquí , hace que sea mucho más agradable de leer, y también funciona en los compiladores de C ++ 03, y depende de su uso enable_ifen el tipo de retorno. Además, el uso enable_ifde parámetros de plantilla de función causa problemas de sobrecarga, porque ahora la firma de la función no es única y causa errores de sobrecarga ambiguos.
Paul Fultz II
3
Esta es una vieja pregunta, pero para cualquiera que todavía esté leyendo: la solución al problema planteado por @Paul es usar enable_ifun parámetro de plantilla no tipo predeterminado, que permite la sobrecarga. Es decir, en enable_if_t<condition, int> = 0lugar de typename = enable_if_t<condition>.
Nir Friedman
enlace de retorno a casi-static-if: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak
@ R.MartinhoFernandes el flamingdangerzoneenlace en su comentario parece conducir a una página de instalación de spyware ahora. Lo marqué para la atención del moderador.
Nispio
58

std::enable_ifse basa en el principio " La falla de la subtítulos no es un error " (también conocido como SFINAE) durante la deducción de argumentos de plantilla . Esta es una característica de lenguaje muy frágil y debe tener mucho cuidado para hacerlo bien.

  1. si su condición dentro del enable_ifcontiene una plantilla anidada o una definición de tipo (sugerencia: busque ::tokens), entonces la resolución de estos tipos o plantillas anidadas generalmente es un contexto no deducido . Cualquier falla de sustitución en un contexto no deducido es un error .
  2. Las diversas condiciones en múltiples enable_ifsobrecargas no pueden tener solapamiento porque la resolución de sobrecarga sería ambigua. Esto es algo que usted, como autor, necesita verificar usted mismo, aunque obtendrá buenas advertencias del compilador.
  3. enable_ifmanipula el conjunto de funciones viables durante la resolución de sobrecarga que puede tener interacciones sorprendentes dependiendo de la presencia de otras funciones que se obtienen de otros ámbitos (por ejemplo, a través de ADL). Esto lo hace no muy robusto.

En resumen, cuando funciona funciona, pero cuando no funciona puede ser muy difícil de depurar. Una muy buena alternativa es usar el despacho de etiquetas , es decir, delegar a una función de implementación (generalmente en un detailespacio de nombres o en una clase auxiliar) que recibe un argumento ficticio basado en la misma condición de tiempo de compilación que usa en el enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

El despacho de etiquetas no manipula el conjunto de sobrecarga, pero lo ayuda a seleccionar exactamente la función que desea al proporcionar los argumentos adecuados a través de una expresión en tiempo de compilación (por ejemplo, en un rasgo de tipo). En mi experiencia, esto es mucho más fácil de depurar y acertar. Si es un aspirante a escritor de bibliotecas de rasgos de tipo sofisticados, es posible que necesite de enable_ifalguna manera, pero para el uso más habitual de las condiciones de tiempo de compilación no se recomienda.

TemplateRex
fuente
22
Sin embargo, el despacho de etiquetas tiene una desventaja: si tiene algún rasgo que detecta la presencia de una función, y esa función se implementa con el enfoque de despacho de etiquetas, siempre informa a ese miembro como presente y genera un error en lugar de una posible falla de sustitución . SFINAE es principalmente una técnica para eliminar sobrecargas de conjuntos candidatos, y el despacho de etiquetas es una técnica para seleccionar entre dos (o más) sobrecargas. Hay cierta superposición en la funcionalidad, pero no son equivalentes.
R. Martinho Fernandes
@ R.MartinhoFernandes, ¿puede dar un breve ejemplo e ilustrar cómo enable_ifhacerlo bien?
TemplateRex
1
@ R.MartinhoFernandes Creo que una respuesta separada que explique estos puntos podría agregar valor al OP. :-) Por cierto, escribir rasgos como is_f_ablees algo que considero una tarea para los escritores de bibliotecas que, por supuesto, pueden usar SFINAE cuando eso les da una ventaja, pero para los usuarios "normales" y con un rasgo is_f_able, creo que el envío de etiquetas es más fácil.
TemplateRex
1
@hansmaad Publiqué una respuesta breve abordando su pregunta, y abordaré el tema "a SFINAE o no SFINAE" en una publicación de blog (está un poco fuera de tema en esta pregunta). Tan pronto como tenga tiempo para terminarlo, quiero decir.
R. Martinho Fernandes
8
SFINAE es "frágil"? ¿Qué?
ligereza corre en órbita
5

¿Qué solución debería preferirse y por qué debería evitar otras?

  • El parámetro de plantilla

    • Es utilizable en constructores.
    • Es utilizable en el operador de conversión definido por el usuario.
    • Requiere C ++ 11 o posterior.
    • Es IMO, el más legible.
    • Se puede usar fácilmente de manera incorrecta y produce errores con sobrecargas:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
      

    Aviso en typename = std::enable_if_t<cond>lugar de correctostd::enable_if_t<cond, int>::type = 0

  • tipo de retorno:

    • No se puede usar en constructor. (sin tipo de devolución)
    • No se puede utilizar en el operador de conversión definido por el usuario. (no deducible)
    • Se puede usar pre-C ++ 11.
    • La segunda OMI más legible.
  • Por último, en el parámetro de función:

    • Se puede usar pre-C ++ 11.
    • Es utilizable en constructores.
    • No se puede utilizar en el operador de conversión definido por el usuario. (sin parámetros)
    • No se puede utilizar en métodos con número fijo de argumentos (/ operadores binarios unarios +, -, *, ...)
    • Se puede usar de forma segura en la herencia (ver más abajo).
    • Cambie la firma de la función (básicamente tiene un argumento adicional como último void* = nullptr) (por lo que el puntero de la función sería diferente, etc.)

¿Hay alguna diferencia para las plantillas de funciones miembro y no miembro?

Hay diferencias sutiles con la herencia y using:

De acuerdo con el using-declarator(énfasis mío):

namespace.udecl

El conjunto de declaraciones introducidas por el declarador de uso se encuentra realizando una búsqueda de nombre calificado ([basic.lookup.qual], [class.member.lookup]) para el nombre en el declarador de uso, excluyendo las funciones que están ocultas como se describe abajo.

...

Cuando un using-declarator trae declaraciones de una clase base a una clase derivada, las funciones miembro y las plantillas de funciones miembro en la clase derivada anulan y / u ocultan funciones miembro y plantillas de funciones miembro con el mismo nombre, lista de tipos de parámetros, cv- calificación y ref-calificador (si corresponde) en una clase base (en lugar de conflictiva). Dichas declaraciones ocultas o anuladas se excluyen del conjunto de declaraciones introducidas por el declarador de uso.

Entonces, tanto para el argumento de plantilla como para el tipo de retorno, los métodos están ocultos en el siguiente escenario:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demostración (gcc encuentra erróneamente la función base).

Mientras que con argumento, un escenario similar funciona:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Manifestación

Jarod42
fuente