Buscar..


Introducción

En C ++, la metaprogramación se refiere al uso de macros o plantillas para generar código en tiempo de compilación.

En general, las macros están mal vistas en este rol y se prefieren las plantillas, aunque no son tan genéricas.

La metaprogramación de plantillas a menudo hace uso de cálculos en tiempo de compilación, ya sea a través de plantillas o funciones constexpr , para lograr sus objetivos de generación de código, sin embargo, los cálculos en tiempo de compilación no son metaprogramación en sí.

Observaciones

La metaprogramación (o más específicamente, la metaprogramación de plantillas) es la práctica de usar plantillas para crear constantes, funciones o estructuras de datos en tiempo de compilación. Esto permite que los cálculos se realicen una vez en tiempo de compilación en lugar de en cada tiempo de ejecución.

Cálculo de factoriales

Los factoriales se pueden calcular en tiempo de compilación utilizando técnicas de metaprogramación de plantillas.

#include <iostream>

template<unsigned int n>
struct factorial
{
    enum
    {
        value = n * factorial<n - 1>::value
    };
};

template<>
struct factorial<0>
{
    enum { value = 1 };
};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

factorial es una estructura, pero en la metaprogramación de plantillas se trata como una metafunción de plantillas. Por convención, las metafunciones de la plantilla se evalúan verificando un miembro en particular, ya sea ::type para las metafunciones que resultan en tipos, o ::value para las metafunciones que generan valores.

En el código anterior, evaluamos la metafunción factorial mediante la creación de una instancia de la plantilla con los parámetros que queremos aprobar, y usando ::value para obtener el resultado de la evaluación.

La metafunción en sí misma se basa en la instanciación recursiva de la misma metafunción con valores más pequeños. La especialización factorial<0> representa la condición de terminación. La metaprogramación de plantillas tiene la mayoría de las restricciones de un lenguaje de programación funcional , por lo que la recursión es la construcción primaria de "bucle".

Dado que las metafunciones de la plantilla se ejecutan en tiempo de compilación, sus resultados se pueden usar en contextos que requieren valores de tiempo de compilación. Por ejemplo:

int my_array[factorial<5>::value];

Las matrices automáticas deben tener un tamaño definido en tiempo de compilación. Y el resultado de una metafunción es una constante de tiempo de compilación, por lo que puede usarse aquí.

Limitación : la mayoría de los compiladores no permitirán una profundidad de recursión más allá de un límite. Por ejemplo, el compilador g++ por defecto limita la recursividad a 256 niveles. En el caso de g++ , el programador puede establecer la profundidad de recursión usando la -ftemplate-depth-X .

C ++ 11

Desde C ++ 11, la plantilla std::integral_constant se puede usar para este tipo de cálculo de plantilla:

#include <iostream>
#include <type_traits>

template<long long n>
struct factorial :
  std::integral_constant<long long, n * factorial<n - 1>::value> {};

template<>
struct factorial<0> :
  std::integral_constant<long long, 1> {};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

Además, constexpr funciones constexpr convierten en una alternativa más limpia.

#include <iostream>

constexpr long long factorial(long long n)
{
  return (n == 0) ? 1 : n * factorial(n - 1);
}

int main()
{
  char test[factorial(3)];
  std::cout << factorial(7) << '\n';
}

El cuerpo de factorial() se escribe como una sola declaración porque en C ++ 11 constexpr funciones constexpr solo pueden usar un subconjunto bastante limitado del lenguaje.

C ++ 14

Desde C ++ 14, se han eliminado muchas restricciones para constexpr funciones constexpr y ahora se pueden escribir mucho más convenientemente:

constexpr long long factorial(long long n)
{
  if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}

O incluso:

constexpr long long factorial(int n)
{
  long long result = 1;
  for (int i = 1; i <= n; ++i) {
    result *= i;
  }
  return result;
}
C ++ 17

Como c ++ 17 se puede usar la expresión de pliegue para calcular factorial:

#include <iostream>
#include <utility>

template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;

template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};

int main() {
   std::cout << factorial<int, 5>::value << std::endl;
}

Iterando sobre un paquete de parámetros

A menudo, necesitamos realizar una operación sobre cada elemento en un paquete de parámetros de plantilla variadic. Hay muchas maneras de hacer esto, y las soluciones son más fáciles de leer y escribir con C ++ 17. Supongamos que simplemente queremos imprimir cada elemento en un paquete. La solución más simple es repetir:

C ++ 11
void print_all(std::ostream& os) {
    // base case
}

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;
    
    print_all(os, rest...);
}

En su lugar, podríamos usar el truco de expansión, para realizar toda la transmisión en una sola función. Esto tiene la ventaja de no necesitar una segunda sobrecarga, pero tiene la desventaja de una legibilidad menor a la estelar:

C ++ 11
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    using expander = int[];
    (void)expander{0,
        (void(os << args), 0)...
    };
}

Para una explicación de cómo funciona esto, vea la excelente respuesta de TC .

C ++ 17

Con C ++ 17, tenemos dos nuevas herramientas poderosas en nuestro arsenal para resolver este problema. El primero es una expresión de plegado:

template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    ((os << args), ...);
}

Y el segundo es if constexpr , que nos permite escribir nuestra solución recursiva original en una sola función:

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;

    if constexpr (sizeof...(rest) > 0) {        
        // this line will only be instantiated if there are further
        // arguments. if rest... is empty, there will be no call to
        // print_all(os). 
        print_all(os, rest...);
    }
}

Iterando con std :: integer_sequence

Desde C ++ 14, el estándar proporciona la plantilla de clase.

template <class T, T... Ints>
class integer_sequence;

template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;

y una metafunción generadora para ello:

template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;

template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;

Si bien esto es estándar en C ++ 14, esto puede implementarse utilizando las herramientas de C ++ 11.


Podemos usar esta herramienta para llamar a una función con un std::tuple de argumentos (estandarizados en C ++ 17 como std::apply ):

namespace detail {
    template <class F, class Tuple, std::size_t... Is>
    decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
        return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
    }
}

template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
    return detail::apply_impl(std::forward<F>(f),
        std::forward<Tuple>(tpl),
        std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}


// this will print 3
int f(int, char, double);

auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)

Despacho de etiquetas

Una forma sencilla de seleccionar entre funciones en tiempo de compilación es enviar una función a un par de funciones sobrecargadas que toman una etiqueta como un argumento (generalmente el último). Por ejemplo, para implementar std::advance() , podemos enviar en la categoría de iterador:

namespace details {
    template <class RAIter, class Distance>
    void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
        it += n;
    }

    template <class BidirIter, class Distance>
    void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
        if (n > 0) {
            while (n--) ++it;
        }
        else {
            while (n++) --it;
        }
    }

    template <class InputIter, class Distance>
    void advance(InputIter& it, Distance n, std::input_iterator_tag) {
        while (n--) {
            ++it;
        }
    }    
}

template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
    details::advance(it, n, 
            typename std::iterator_traits<Iter>::iterator_category{} );
}

Los argumentos std::XY_iterator_tag de los details::advance sobrecargados details::advance funciones details::advance son parámetros de función no utilizados. La implementación real no importa (en realidad está completamente vacía). Su único propósito es permitir que el compilador seleccione una sobrecarga en función de los details::advance clase de etiqueta details::advance se llama a details::advance .

En este ejemplo, advance utiliza los iterator_traits<T>::iterator_category metafunción que devuelve uno de los iterator_tag clases, dependiendo del tipo real de Iter . Un objeto construido por defecto del tipo iterator_category<Iter>::type permite al compilador seleccionar una de las diferentes sobrecargas de details::advance . (Es probable que este parámetro de función se optimice por completo, ya que es un objeto construido por defecto de una struct vacía y nunca se usa).

El envío de etiquetas puede darle un código que es mucho más fácil de leer que los equivalentes utilizando SFINAE y enable_if .

Nota: mientras que C ++ 17, if constexpr puede simplificar la implementación del advance en particular, no es adecuado para implementaciones abiertas a diferencia del envío de etiquetas.

Detectar si la expresión es válida

Es posible detectar si se puede llamar a un operador o función en un tipo. Para probar si una clase tiene una sobrecarga de std::hash , uno puede hacer esto:

#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval

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

template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
    : std::true_type
{};
C ++ 17

Desde C ++ 17, std::void_t puede usarse para simplificar este tipo de construcción

#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval

template<class, class = std::void_t<> >
struct has_hash
    : std::false_type
{};

template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
    : std::true_type
{};

donde std::void_t se define como:

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

Para detectar si un operador, como el operator< está definido, la sintaxis es casi la misma:

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

template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
    : std::true_type
{};

Se pueden usar para usar std::unordered_map<T> si T tiene una sobrecarga para std::hash , pero de lo contrario intente usar std::map<T> :

template <class K, class V>
using hash_invariant_map = std::conditional_t<
    has_hash<K>::value,
    std::unordered_map<K, V>,
    std::map<K,V>>;    

Cálculo de la potencia con C ++ 11 (y superior)

Con C ++ 11 y los cálculos superiores en tiempo de compilación puede ser mucho más fácil. Por ejemplo, el cálculo de la potencia de un número dado en el momento de la compilación será el siguiente:

template <typename T>
constexpr T calculatePower(T value, unsigned power) {
    return power == 0 ? 1 : value * calculatePower(value, power-1);
}

Keyword constexpr es responsable de calcular la función en el tiempo de compilación, entonces y solo entonces, cuando se cumplan todos los requisitos para esto (ver más en la referencia de la palabra clave constexpr), por ejemplo, todos los argumentos deben ser conocidos en el momento de la compilación.

Nota: En C ++ 11, la función constexpr debe componerse solo desde una declaración de retorno.

Ventajas: Comparando esto con la forma estándar de cálculo del tiempo de compilación, este método también es útil para los cálculos de tiempo de ejecución. Esto significa que si los argumentos de la función no se conocen en el momento de la compilación (por ejemplo, el valor y la potencia se dan como entrada a través del usuario), entonces la función se ejecuta en un tiempo de compilación, por lo que no es necesario duplicar un código (ya que sería forzado en los estándares más antiguos de C ++).

P.ej

void useExample() {
    constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
                               // as both arguments are known at compilation time
                               // and used for a constant expression.
    int value;
    std::cin >> value;
    int runtimeCalculated = calculatePower(value, 3);  // runtime calculated,
                                    // because value is known only at runtime.
}
C ++ 17

Otra forma de calcular la potencia en tiempo de compilación puede utilizar la expresión de plegado de la siguiente manera:

#include <iostream>
#include <utility>

template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;

template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};

int main() {
   std::cout << power<int, 4, 2>::value << std::endl;
}

Distinción manual de los tipos cuando se da cualquier tipo T

Al implementar SFINAE utilizando std::enable_if , a menudo es útil tener acceso a las plantillas de ayuda que determinan si un tipo T dado coincide con un conjunto de criterios.

Para ayudarnos con eso, el estándar ya proporciona dos tipos analógicos a true y false que son std::true_type y std::false_type .

El siguiente ejemplo muestra cómo detectar si un tipo T es un puntero o no, la plantilla is_pointer imita el comportamiento del std::is_pointer estándar std::is_pointer helper:

template <typename T>
struct is_pointer_: std::false_type {};

template <typename T>
struct is_pointer_<T*>: std::true_type {};

template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }

Hay tres pasos en el código anterior (a veces solo necesitas dos):

  1. La primera declaración de is_pointer_ es el caso predeterminado y se hereda de std::false_type . El caso predeterminado siempre debe heredarse de std::false_type ya que es análogo a una "condición false ".

  2. La segunda declaración especializa la plantilla is_pointer_ para el puntero T* sin importar qué es realmente T Esta versión hereda de std::true_type .

  3. La tercera declaración (la real) simplemente elimina cualquier información innecesaria de T (en este caso, eliminamos volatile calificadores const y volatile ) y luego retrocedemos a una de las dos declaraciones anteriores.

Dado que is_pointer<T> es una clase, para acceder a su valor necesita:

  • Use ::value , por ejemplo, is_pointer<int>::value - value es un miembro de clase estática de tipo bool heredado de std::true_type o std::false_type ;
  • Construya un objeto de este tipo, por ejemplo, is_pointer<int>{} - Esto funciona porque std::is_pointer hereda su constructor predeterminado de std::true_type o std::false_type (que tienen constexpr constructores) y std::true_type y std::false_type tiene constexpr operadores de conversión a bool .

Es un buen hábito proporcionar "plantillas de ayuda auxiliar" que le permiten acceder directamente al valor:

template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17

En C ++ 17 y superior, la mayoría de las plantillas de ayuda ya proporcionan una versión _v , por ejemplo:

template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;

Si-entonces-de lo contrario

C ++ 11

El tipo std::conditional en el encabezado de la biblioteca estándar <type_traits> puede seleccionar un tipo u otro, basado en un valor booleano en tiempo de compilación:

template<typename T>
struct ValueOrPointer
{
    typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};

Esta estructura contiene un puntero a T si T es más grande que el tamaño de un puntero, o T si es más pequeño o igual al tamaño de un puntero. Por lo tanto, sizeof(ValueOrPointer) siempre será <= sizeof(void*) .

Generic Min / Max con cuenta de argumento variable

C ++ 11

Es posible escribir una función genérica (por ejemplo, min ) que acepte varios tipos numéricos y un conteo de argumentos arbitrarios mediante la metaprogramación de la plantilla. Esta función declara un min para dos argumentos y recursivamente para más.

template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b) 
-> typename std::common_type<const T1&, const T2&>::type
{
    return a < b ? a : b;
}

template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
    return min(min(a, b), args...);
}

auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);


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