C++
Metaprogramacion
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
.
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.
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;
}
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:
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:
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 .
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
{};
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.
}
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):
La primera declaración de
is_pointer_
es el caso predeterminado y se hereda destd::false_type
. El caso predeterminado siempre debe heredarse destd::false_type
ya que es análogo a una "condiciónfalse
".La segunda declaración especializa la plantilla
is_pointer_
para el punteroT*
sin importar qué es realmenteT
Esta versión hereda destd::true_type
.La tercera declaración (la real) simplemente elimina cualquier información innecesaria de
T
(en este caso, eliminamosvolatile
calificadoresconst
yvolatile
) 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 tipobool
heredado destd::true_type
ostd::false_type
; - Construya un objeto de este tipo, por ejemplo,
is_pointer<int>{}
- Esto funciona porquestd::is_pointer
hereda su constructor predeterminado destd::true_type
ostd::false_type
(que tienenconstexpr
constructores) ystd::true_type
ystd::false_type
tieneconstexpr
operadores de conversión abool
.
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;
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
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
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);