c++ - Restringir argumentos de plantillas variadic
c++ variadic templates (5)
¿Podemos restringir los argumentos de plantilla variadic a cierto tipo? Es decir, lograr algo como esto (C ++ no real, por supuesto):
struct X {};
auto foo(X... args)
Aquí mi intención es tener una función que acepte un número variable de parámetros X
Lo más cercano que tenemos es esto:
template <class... Args>
auto foo(Args... args)
pero esto acepta cualquier tipo de parámetro.
C ++ 14
Desde C ++ 14 puedes usar también plantillas de variables , especialización parcial y static_assert
para hacer eso. Como ejemplo:
#include <type_traits>
template<template<typename...> class, typename...>
constexpr bool check = true;
template<template<typename...> class C, typename U, typename T, typename... O>
constexpr bool check<C, U, T, O...> = C<T, U>::value && check<C, U, O...>;
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible, int, T...>, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won''t work, for S is not convertible to int
// f<int, S, int>();
}
También puede usar check
junto con std::enable_if_t
como tipo de devolución, si no desea usar static_assert
por algún motivo desconocido:
template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
// ...
}
Y así...
C ++ 11
En C ++ 11, también puede diseñar una solución que detenga la recursión inmediatamente cuando se encuentre un tipo que no se va a aceptar. Como ejemplo:
#include <type_traits>
template<bool...> struct check;
template<bool... b> struct check<false, b...>: std::false_type {};
template<bool... b> struct check<true, b...>: check<b...> {};
template<> struct check<>: std::true_type {};
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible<int, T>::value...>::value, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won''t work, for S is not convertible to int
// f<int, S, int>();
}
Como se mencionó anteriormente, puede usar también check
en el tipo de devolución o donde quiera.
¿Qué hay de la siguiente solución?
--- EDITAR --- Mejorado siguiendo la sugerencia de bolov y Jarod42 (¡gracias!)
#include <iostream>
template <typename ... Args>
auto foo(Args... args) = delete;
auto foo ()
{ return 0; }
template <typename ... Args>
auto foo (int i, Args ... args)
{ return i + foo(args...); }
int main ()
{
std::cout << foo(1, 2, 3, 4) << std::endl; // compile because all args are int
//std::cout << foo(1, 2L, 3, 4) << std::endl; // error because 2L is long
return 0;
}
Puede declarar foo()
para recibir todos los tipos de argumentos ( Args ... args
) pero (recursivamente) implementarlo solo para un tipo ( int
en este ejemplo).
Qué static_assert
método static_assert
y helper template (solución c ++ 11):
template <bool b>
int assert_impl() {
static_assert(b, "not convertable");
return 0;
}
template <class... Args>
void foo_x(Args... args) {
int arr[] {assert_impl<std::is_convertible<Args, X>::value>()...};
(void)arr;
}
Uno más c ++ 11 este usa solución "one-liner" basada en sfinae:
template <class... Args,
class Enable = decltype(std::array<int, sizeof...(Args)>{typename std::enable_if<std::is_convertible<Args, X>::value, int>::type{}...})>
void foo_x(Args... args) {
}
Sí, es posible. En primer lugar, debe decidir si desea aceptar solo el tipo, o si desea aceptar un tipo implícitamente convertible. Utilizo std::is_convertible
en los ejemplos porque imita mejor el comportamiento de los parámetros que no son de plantilla, por ejemplo, un parámetro long long
aceptará un argumento int
. Si por alguna razón necesita solo ese tipo para ser aceptado, reemplace std::is_convertible
con std:is_same
(puede que necesite agregar std::remove_reference
y std::remove_cv
).
Desafortunadamente, en C++
reducción de la conversión, por ejemplo ( long long
to int
e incluso duble
to int
) son conversiones implícitas. Y si bien en una configuración clásica puede recibir advertencias cuando ocurren, no la obtiene con std::is_convertible
. Al menos no en la llamada. Puede obtener las advertencias en el cuerpo de la función si realiza dicha tarea. Pero con un pequeño truco también podemos obtener el error en el sitio de llamadas con plantillas.
Así que sin más preámbulos aquí va:
La plataforma de prueba:
struct X {};
struct Derived : X {};
struct Y { operator X() { return {}; }};
struct Z {};
foo_x : function that accepts X arguments
int main ()
{
int i{};
X x{};
Derived d{};
Y y{};
Z z{};
foo_x(x, x, y, d); // should work
foo_y(x, x, y, d, z); // should not work due to unrelated z
};
Conceptos
Aún no está aquí, pero pronto. Esta será la solución más simple, clara y elegante
template <class From, class To>
concept constexpr bool Convertible = std::is_convertible_v<From, To>;
template <Convertible<X>... Args>
auto foo_x(Args... args) {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // error:
Obtenemos un error muy bueno. Especialmente el
''Convertible'' no estaba satisfecho
es dulce:
error: cannot call function ''auto foo_x(Args ...) [with Args = {X, X, Y, Derived, Z}]'' foo_x(x, x, y, d, z); ^ note: constraints not satisfied auto foo_x(Args... args) ^~~~~ note: in the expansion of ''Convertible<Args, X>...'' note: ''Convertible<Z, X>'' was not satisfied
Lidiar con el estrechamiento
template <class From, class To>
concept constexpr bool Convertible_no_narrow = requires(From f, To t) {
t = {f};
};
template <Convertible_no_narrow<int>... Args>
auto foo_ni(Args... args) {}
foo_ni(24, 12); // OK
foo_ni(24, 12, 15.2);
// error:
// ''Convertible_no_narrow<double, int>'' was not satisfied
C ++ 17
Hacemos uso de la expresión de doblez muy agradable:
template <class... Args,
class Enable = std::enable_if_t<(... && std::is_convertible_v<Args, X>)>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d, z); // OK
foo_x(x, x, y, d, z, d); // error
Desafortunadamente obtenemos un error menos claro:
deducción / sustitución del argumento de la plantilla fallida: [...]
Estrechamiento
Podemos evitar el estrechamiento, pero tenemos que cocinar un rasgo is_convertible_no_narrowing
(tal vez nombrarlo de manera diferente):
template <class From, class To>
struct is_convertible_no_narrowing_impl {
template <class F, class T,
class Enable = decltype(std::declval<T &>() = {std::declval<F>()})>
static auto test(F f, T t) -> std::true_type;
static auto test(...) -> std::false_type;
static constexpr bool value =
decltype(test(std::declval<From>(), std::declval<To>()))::value;
};
template <class From, class To>
struct is_convertible_no_narrowing
: std::integral_constant<
bool, is_convertible_no_narrowing_impl<From, To>::value> {};
C ++ 14
Creamos un auxiliar de conjunción:
tenga en cuenta que en C++17
habrá una std::conjunction
, pero tomará argumentos std::integral_constant
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
y ahora podemos tener nuestra función:
template <class... Args,
class Enable = std::enable_if_t<
conjunction<std::is_convertible<Args, X>::value...>::value>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
C ++ 11
solo pequeños ajustes a la versión C ++ 14:
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
template <class... Args,
class Enable = typename std::enable_if<
conjunction<std::is_convertible<Args, X>::value...>::value>::type>
auto foo_x(Args... args) -> void {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
Ya lo tienes desde el estándar C ++ 11.
Un simple std::array
(caso especial de std::tuple
donde todos los elementos de tupla comparten el mismo tipo) será suficiente.
Sin embargo, si desea utilizarlo en una función de plantilla, puede utilizar mejor una''std :: initializer_list` como en el siguiente ejemplo:
template< typename T >
void foo( std::initializer_list<T> elements );
Esta es una solución realmente simple que resuelve tu problema. El uso de argumentos de plantilla variadic también es una opción, pero agrega complejidad innecesaria a su código. Recuerde que su código debe ser legible por otros, incluido usted mismo después de un tiempo.