Pouvons-nous restreindre les arguments de modèles variadiques à un certain type? C'est-à-dire, réaliser quelque chose comme ça (pas du vrai C++ bien sûr):
struct X {};
auto foo(X... args)
Ici, mon intention est d'avoir une fonction qui accepte un nombre variable de paramètres X
.
Le plus proche que nous avons est le suivant:
template <class... Args>
auto foo(Args... args)
mais cela accepte tout type de paramètre.
Oui c'est possible. Tout d'abord, vous devez décider si vous souhaitez accepter uniquement le type ou si vous souhaitez accepter un type implicitement convertible. J'utilise std::is_convertible
Dans les exemples car il imite mieux le comportement des paramètres non modèles, par exemple un paramètre long long
acceptera un argument int
. Si, pour une raison quelconque, vous avez juste besoin de ce type pour être accepté, remplacez std::is_convertible
Par std:is_same
(Vous devrez peut-être ajouter std::remove_reference
Et std::remove_cv
).
Malheureusement, dans C++
Réduction de la conversion, par exemple (long long
En int
et même double
en int
) sont des conversions implicites. Et tandis que dans une configuration classique, vous pouvez obtenir des avertissements lorsque cela se produit, vous n'obtenez pas cela avec std::is_convertible
. Du moins pas à l'appel. Vous pouvez obtenir les avertissements dans le corps de la fonction si vous effectuez une telle affectation. Mais avec une petite astuce, nous pouvons également obtenir l'erreur sur le site d'appel avec des modèles.
Donc, sans plus tarder, ça va:
Le banc d'essai:
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
};
Pas encore là, mais bientôt. Disponible en coffre gcc (mars 2020). C'est la solution la plus simple, claire, élégante et sûre:
#include <concepts>
auto foo(std::convertible_to<X> auto ... args) {}
foo(x, x, y, d); // OK
foo(x, x, y, d, z); // error:
Nous obtenons une très belle erreur. En particulier le
contraintes non satisfaites
est doux:
Je n'ai pas trouvé de concept dans la bibliothèque, nous devons donc en créer un:
template <class From, class To>
concept ConvertibleNoNarrowing = std::convertible_to<From, To>
&& requires(void (*foo)(To), From f) {
foo({f});
};
auto foo_ni(ConvertibleNoNarrowing<int> auto ... args) {}
foo_ni(24, 12); // OK
foo_ni(24, (short)12); // OK
foo_ni(24, (long)12); // error
foo_ni(24, 12, 15.2); // error
Nous utilisons le très Nice expression de pli :
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
Malheureusement, nous obtenons une erreur moins claire:
échec de la déduction/substitution d'un argument de modèle: [...]
Nous pouvons éviter de rétrécir, mais nous devons cuisiner un trait is_convertible_no_narrowing
(Peut-être le nommer différemment):
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> {};
Nous créons un assistant de conjonction:
veuillez noter que dans C++17
, il y aura un std::conjunction
, mais cela prendra des arguments 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> {};
et maintenant nous pouvons avoir notre fonction:
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
quelques ajustements mineurs à la version 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
Depuis C++ 14, vous pouvez également utiliser modèle de variable, spécialisation partielle et static_assert
pour faire ça. Par exemple:
#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>();
}
Vous pouvez également utiliser check
en conjonction avec std::enable_if_t
comme type de retour, si vous ne souhaitez pas utiliser static_assert
pour des raisons inconnues:
template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
// ...
}
Etc...
En C++ 11, vous pouvez également concevoir une solution qui arrête la récursivité immédiatement lorsqu'un type qui ne doit pas être accepté est rencontré. Par exemple:
#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>();
}
Comme mentionné ci-dessus, vous pouvez également utiliser check
dans le type de retour ou où vous le souhaitez.
Qu'en est-il de la solution suivante?
--- EDIT --- Amélioration de la suggestion suivante de bolov et Jarod42 (merci!)
#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;
}
Vous pouvez déclarer foo()
pour recevoir tous les types d'arguments (Args ... args
) Mais (récursivement) l'implémenter uniquement pour un type (int
dans cet exemple).
Que diriez-vous static_assert
et méthode de modèle d'assistance (solution 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;
}
Un autre c ++ 11 celui-ci utilise une solution à base de sfinae "one-liner":
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) {
}
Vous l'avez déjà depuis la norme C++ 11.
Un simple std::array
(cas particulier de std::Tuple
où tous les éléments Tuple partagent le même type) sera suffisant.
Cependant, si vous souhaitez l'utiliser dans une fonction de modèle, vous pouvez mieux utiliser un ´std :: initializer_list` comme dans l'exemple suivant:
template< typename T >
void foo( std::initializer_list<T> elements );
Il s'agit d'une solution vraiment simple qui résout votre problème. L'utilisation d'arguments de modèles variadiques est également une option, mais ajoute une complexité inutile à votre code. N'oubliez pas que votre code doit être lisible par les autres, y compris vous-même après un certain temps.