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

Scott Meyers publicó el contenido y el estado de su próximo libro EC ++ 11. Escribió que un elemento en el libro podría ser “Avoid std::enable_if in function std::enable_if .

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

En esta pregunta , se muestran las tres soluciones.

Como parámetro de función:

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

Como parámetro de plantilla:

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

Como tipo de devolución:

 template struct Check3 { template typename std::enable_if<std::is_same::value, U>::type read() { return 42; } template typename std::enable_if<std::is_same::value, U>::type read() { return 3.14; } }; 
  • ¿Qué solución debería preferirse y por qué debería evitar otras?
  • ¿En qué casos “Avoid std::enable_if in function std::enable_if concierne el uso como tipo de devolución (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?

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

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

  • legibilidad : los tipos enable_if use y return / argument no se fusionan en un solo trozo desordenado de nombre de tipo desambiguadores y accesos de tipo nesteds; a pesar de que el desorden del desambiguador y el tipo nested se puede mitigar con plantillas de alias, eso aún combinaría dos cosas no relacionadas entre sí. El uso de enable_if está relacionado con los parámetros de la plantilla, no con los tipos de devolución. 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 devolución, y algunos operadores no pueden tener argumentos adicionales, por lo que ninguna de las otras dos opciones puede aplicarse en todas partes. Poner enable_if en un parámetro de plantilla funciona en todas partes ya que solo puede usar SFINAE en las plantillas de todos modos.

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

std::enable_if basa en el principio de “la falla de substición no es un error ” (también conocido como SFINAE) durante la deducción del argumento de la plantilla . Esta es una característica del lenguaje muy frágil y debe tener mucho cuidado para hacerlo bien.

  1. si su condición dentro de enable_if contiene una plantilla anidada o definición de tipo (hint: look para :: tokens), entonces la resolución de estos tempatles o tipos nesteds 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 sobrecargas enable_if no pueden superponerse porque la resolución de sobrecarga sería ambigua. Esto es algo que usted, como autor, necesita comprobar usted mismo, aunque obtendrá buenas advertencias de comstackción.
  3. enable_if manipula el conjunto de funciones viables durante la resolución de sobrecarga, que puede tener interacciones sorprendentes dependiendo de la presencia de otras funciones que provienen de otros ámbitos (por ejemplo, a través de ADL). Esto hace que no sea 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 tags , es decir, delegar en una función de implementación (generalmente en un espacio de nombres de detail o en una clase auxiliar) que recibe un argumento ficticio basado en la misma condición de tiempo de comstackción que usa en enable_if .

 template T fun(T arg) { return detail::fun(arg, typename some_template_trait::type() ); } namespace detail { template fun(T arg, std::false_type /* dummy */) { } template fun(T arg, std::true_type /* dummy */) {} } 

El despacho de tags no manipula el conjunto de sobrecargas, pero le ayuda a seleccionar exactamente la función que desea proporcionando los argumentos adecuados a través de una expresión en tiempo de comstackción (por ejemplo, en un rasgo de tipo). En mi experiencia, esto es mucho más fácil de depurar y hacer las cosas bien. Si usted es un escritor de la biblioteca aspirante de rasgos de tipo sofisticado, es posible que necesite enable_if alguna manera, pero para el uso más regular de las condiciones de tiempo de comstackción no es recomendable.

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

  • El parámetro de la plantilla

    • Se puede usar en constructores (sin tipo de devolución).
    • 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::value>> void f() {/*...*/} template::value>> void f() {/*...*/} // Redefinition: both are just template f() 

    Aviso typename = std::enable_if_t lugar de std::enable_if_t::type = 0 correcto std::enable_if_t::type = 0

  • tipo de devolución:

    • No puede usarse en constructor.
    • Se puede usar antes de C ++ 11.
    • Segunda OMI más legible
  • Por último, en el parámetro de función:

    • Se puede usar antes de C ++ 11.
    • Se puede usar de forma segura en herencia (ver a continuación).
    • Cambiar 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, y así sucesivamente)

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

Hay diferencias sutiles con la herencia y el using :

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

namespace.udecl

El conjunto de declaraciones introducidas por el declarador de uso se encuentra al realizar la 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 las funciones miembro y las plantillas de funciones miembro con el mismo nombre, parameter-type-list, cv- calificación y ref-calificador (si corresponde) en una clase base (en lugar de estar en conflicto). Tales declaraciones ocultas o anuladas se excluyen del conjunto de declaraciones introducidas por el declarante-usuario.

Entonces, tanto para el argumento de la plantilla como para el tipo de retorno, los métodos que se ocultan son los siguientes:

 struct Base { template * = nullptr> void f() {} template  std::enable_if_t g() {} }; struct S : Base { using Base::f; // Useless, f<0> is still hidden using Base::g; // Useless, g<0> is still hidden template * = nullptr> void f() {} template  std::enable_if_t g() {} }; 

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

Mientras que con argumento, un escenario similar funciona:

 struct Base { template  void h(std::enable_if_t* = nullptr) {} }; struct S : Base { using Base::h; // Base::h<0> is visible template  void h(std::enable_if_t* = nullptr) {} }; 

Manifestación