lenguaje ejecutar compilar compilador compilado c++ templates if-statement c++11 typetraits

c++ - ejecutar - gcc linux



¿Qué hacen los compiladores con la ramificación en tiempo de compilación? (4)

TL; DR

Hay varias formas de obtener un comportamiento de tiempo de ejecución diferente en función de un parámetro de plantilla. El rendimiento no debe ser su principal preocupación aquí, pero sí la flexibilidad y la capacidad de mantenimiento. En todos los casos, las diversas envolturas delgadas y expresiones condicionales constantes se optimizarán en cualquier compilador decente para compilaciones de versiones. Debajo de un pequeño resumen con las diferentes compensaciones (inspirado en esta respuesta por @AndyProwl).

Tiempo de ejecución si

Su primera solución es el tiempo de ejecución simple if :

template<class T> T numeric_procedure(const T& x) { if (std::is_integral<T>::value) { // valid code for integral types } else { // valid code for non-integral types, // must ALSO compile for integral types } }

Es simple y efectivo: cualquier compilador decente optimizará la rama muerta.

Hay varias desventajas:

  • en algunas plataformas (MSVC), una expresión condicional constante produce una advertencia de compilador espurio que luego debe ignorar o silenciar.
  • Pero, lo que es peor, en todas las plataformas conformes, ambas ramas de la instrucción if/else necesitan compilar realmente para todos los tipos T , incluso si se sabe que una de las ramas no se toma. Si T contiene diferentes tipos de miembros dependiendo de su naturaleza, entonces obtendrá un error de compilación tan pronto como intente acceder a ellos.

Despacho de etiquetas

Su segundo enfoque se conoce como despacho de etiquetas:

template<class T> T numeric_procedure_impl(const T& x, std::false_type) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template<class T> T numeric_procedure_impl(const T& x, std::true_type) { // valid code for integral types } template<class T> T numeric_procedure(const T& x) { return numeric_procedure_impl(x, std::is_integral<T>()); }

Funciona bien, sin sobrecarga de tiempo de ejecución: el std::is_integral<T>() temporal y la llamada a la función auxiliar de una línea se optimizarán en cualquier plataforma decente.

La principal desventaja (menor IMO) es que tienes un modelo repetitivo con 3 en lugar de 1 función.

SFINAE

Estrechamente relacionado con el despacho de etiquetas está SFINAE (la falla de sustitución no es un error)

template<class T, class = typename std::enable_if<!std::is_integral<T>::value>::type> T numeric_procedure(const T& x) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template<class T, class = typename std::enable_if<std::is_integral<T>::value>::type> T numeric_procedure(const T& x) { // valid code for integral types }

Esto tiene el mismo efecto que el despacho de etiquetas, pero funciona de forma ligeramente diferente. En lugar de usar la deducción de argumento para seleccionar la sobrecarga de ayuda adecuada, manipula directamente el conjunto de sobrecarga para su función principal.

La desventaja es que puede ser una forma frágil y complicada si no se sabe exactamente cuál es el conjunto de sobrecarga completo (por ejemplo, con código pesado de plantilla, ADL podría atraer más sobrecargas de espacios de nombres asociados en los que no se haya pensado ). Y en comparación con el despacho de etiquetas, la selección basada en cualquier cosa que no sea una decisión binaria es mucho más complicado.

Especialización parcial

Otro enfoque es utilizar un asistente de plantilla de clase con un operador de aplicación de funciones y especializarlo parcialmente

template<class T, bool> struct numeric_functor; template<class T> struct numeric_functor<T, false> { T operator()(T const& x) const { // valid code for non-integral types, // CAN contain code that is invalid for integral types } }; template<class T> struct numeric_functor<T, true> { T operator()(T const& x) const { // valid code for integral types } }; template<class T> T numeric_procedure(T const& x) { return numeric_functor<T, std::is_integral<T>::value>()(x); }

Este es probablemente el enfoque más flexible si desea tener un control detallado y una duplicación mínima del código (por ejemplo, si también desea especializarse en tamaño y / o alineación, pero solo para tipos de coma flotante). La coincidencia de patrones dada por la especialización de plantilla parcial es ideal para tales problemas avanzados. Al igual que con el despacho de etiquetas, los funtores auxiliares son optimizados por cualquier compilador decente.

La principal desventaja es la placa de caldera ligeramente más grande si solo desea especializarse en una sola condición binaria.

Si constexpr (propuesta C ++ 1z)

Este es un reboot de propuestas anteriores fallidas para static if (que se usa en el lenguaje de programación D)

template<class T> T numeric_procedure(const T& x) { if constexpr (std::is_integral<T>::value) { // valid code for integral types } else { // valid code for non-integral types, // CAN contain code that is invalid for integral types } }

Al igual que con su tiempo de ejecución, if , todo está en un solo lugar, pero la principal ventaja aquí es que la rama else será eliminada por completo por el compilador cuando se sabe que no se toma. Una gran ventaja es que mantiene todo el código local, y no tiene que usar pequeñas funciones auxiliares como en el despacho de etiquetas o la especialización de plantillas parciales.

Conceptos-Lite (propuesta de C ++ 1z)

Concepts-Lite es una próxima especificación técnica que está programada para ser parte de la próxima versión principal de C ++ (C ++ 1z, con z==7 como la mejor suposición).

template<Non_integral T> T numeric_procedure(const T& x) { // valid code for non-integral types, // CAN contain code that is invalid for integral types } template<Integral T> T numeric_procedure(const T& x) { // valid code for integral types }

Este enfoque reemplaza la palabra clave typename o typename dentro de los paréntesis de template< > con un nombre de concepto que describe la familia de tipos para los que se supone que el código debe funcionar. Se puede ver como una generalización de las técnicas de envío de etiquetas y SFINAE. Algunos compiladores (gcc, Clang) tienen soporte experimental para esta característica. El adjetivo Lite se refiere a la propuesta fallida de Conceptos C ++ 11.

EDITAR: Tomé el caso "if / else" como un ejemplo que a veces puede resolverse en tiempo de compilación (por ejemplo, cuando se trata de valores estáticos, cf <type_traits> ). Adaptar las respuestas a continuación a otros tipos de ramificación estática (por ejemplo, ramas múltiples o ramas de criterios múltiples) debería ser sencillo. Tenga en cuenta que la ramificación en tiempo de compilación utilizando la programación de plantilla-meta no es el tema aquí.

En un código típico como este

#include <type_traits> template <class T> T numeric_procedure( const T& x ) { if ( std::is_integral<T>::value ) { // Integral types } else { // Floating point numeric types } }

¿optimizará el compilador la instrucción if / else cuando defino tipos de plantilla específicos más adelante en mi código?

Una alternativa simple sería escribir algo como esto:

#include <type_traits> template <class T> inline T numeric_procedure( const T& x ) { return numeric_procedure_impl( x, std::is_integral<T>() ); } // ------------------------------------------------------------------------ template <class T> T numeric_procedure_impl( const T& x, std::true_type const ) { // Integral types } template <class T> T numeric_procedure_impl( const T& x, std::false_type const ) { // Floating point numeric types }

¿Hay alguna diferencia en términos de rendimiento entre estas soluciones? ¿Hay motivos no subjetivos para decir que uno es mejor que el otro? ¿Hay otras soluciones (posiblemente mejores) para tratar con la ramificación en tiempo de compilación?


Crédito a @MooingDuck y @Casey

template<class FN1, class FN2, class ...Args> decltype(auto) if_else_impl(std::true_type, FN1 &&fn1, FN2 &&, Args&&... args) { return fn1(std::forward<Args>(args)...); } template<class FN1, class FN2, class ...Args> decltype(auto) if_else_impl(std::false_type, FN1 &&, FN2 &&fn2, Args&&... args) { return fn2(std::forward<Args>(args)...); } #define static_if(...) if_else_impl(__VA_ARGS__, *this)

Y uso tan simple como:

static_if(do_it, [&](auto& self){ return 1; }, [&](auto& self){ return self.sum(2); } );

Funciona como estático si el compilador va solo a la rama "verdadera".

PD Debes tener self = *this y hacer llamadas de miembros desde allí, debido a un error de gcc . Si tiene llamadas lambda anidadas, no puede usar this-> lugar de self.


Tenga en cuenta que, aunque el optimizador puede ser capaz de eliminar pruebas conocidas y ramas inaccesibles del código generado, el compilador aún debe poder compilar cada rama.

Es decir:

int foo() { #if 0 return std::cout << "this isn''t going to work/n"; #else return 1; #endif }

funcionará bien, porque el preprocesador elimina la rama muerta antes de que el compilador la vea, pero:

int foo() { if (std::is_integral<double>::value) { return std::cout << "this isn''t going to work/n"; } else { return 1; } }

no lo hará Aunque el optimizador puede descartar la primera rama, no podrá compilarse. Aquí es donde se usa enable_if y la ayuda de SFINAE, porque puede seleccionar el código válido (compilable) y la falla del Código no compilable (no compilable) no es un error.


El compilador puede ser lo suficientemente inteligente como para ver que puede reemplazar el cuerpo de la declaración if con dos implementaciones de funciones diferentes, y simplemente elegir la correcta. Pero a partir de 2014 dudo que haya un compilador lo suficientemente inteligente como para hacer eso. Yo podría, sin embargo, estar equivocado. std::is_integral , std::is_integral es tan simple que creo que se optimizará.

Su idea de sobrecargar el resultado de std::is_integral es una posible solución.

Otra solución limpiadora de IMHO es usar std::enable_if (junto con std::is_integral ).