Restreindre les arguments des modèles variadiques

Peut-on restreindre les arguments des modèles variadiques à un certain type? Ie, réaliser quelque chose comme ça (pas de C ++ réel bien sûr):

struct X {}; auto foo(X... args) 

Ici, mon intention est d’avoir une fonction qui accepte un nombre variable de parameters X

Le plus proche est celui-ci:

 template  auto foo(Args... args) 

mais cela accepte n’importe quel 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 cela imite mieux le comportement des parameters non std::is_convertible sur des modèles, par exemple un long long paramètre long long acceptera un argument int . Si pour une raison quelconque vous avez besoin de ce type, remplacez std::is_convertible par std:is_same (vous devrez peut-être append std::remove_reference et std::remove_cv ).

Malheureusement, en C++ conversions implicites sont des conversions implicites. Et bien que dans une installation classique, vous pouvez recevoir des avertissements lorsque ceux-ci se produisent, vous ne les obtenez pas avec std::is_convertible . Au moins pas à l’appel. Vous pourriez 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, le voici:


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 }; 

Les concepts

Pas encore là, mais bientôt. Ce sera la solution la plus simple, claire et élégante

 template  concept constexpr bool Convertible = std::is_convertible_v; template ... Args> auto foo_x(Args... args) {} foo_x(x, x, y, d); // OK foo_x(x, x, y, d, z); // error: 

Nous obtenons une très belle erreur. En particulier le

‘Convertible’ n’était pas satisfait

est doux:

 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...' note: 'Convertible' was not satisfied 

Traiter avec le rétrécissement:

 template  concept constexpr bool Convertible_no_narrow = requires(From f, To t) { t = {f}; }; template ... Args> auto foo_ni(Args... args) {} foo_ni(24, 12); // OK foo_ni(24, 12, 15.2); // error: // 'Convertible_no_narrow' was not satisfied 

C ++ 17

Nous utilisons la très belle expression de pli :

 template )>> 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:

la déduction / substitution d’argument de modèle a échoué: […]

Rétrécissement

Nous pouvons éviter de rétrécir, mais nous devons faire cuire un trait is_convertible_no_narrowing (peut-être le nommer différemment):

 template  struct is_convertible_no_narrowing_impl { template () = {std::declval()})> static auto test(F f, T t) -> std::true_type; static auto test(...) -> std::false_type; static constexpr bool value = decltype(test(std::declval(), std::declval()))::value; }; template  struct is_convertible_no_narrowing : std::integral_constant< bool, is_convertible_no_narrowing_impl::value> {}; 

C ++ 14

Nous créons un assistant de conjonction:
veuillez noter qu’en C++17 il y aura une std::conjunction , mais il faudra std::integral_constant arguments std::integral_constant

 template  struct conjunction {}; template  struct conjunction : std::integral_constant::value>{}; template  struct conjunction : std::integral_constant {}; 

et maintenant nous pouvons avoir notre fonction:

 template ::value...>::value>> auto foo_x(Args... args) {} foo_x(x, x, y, d); // OK foo_x(x, x, y, d, z); // Error 

C ++ 11

juste des modifications mineures à la version C ++ 14:

 template  struct conjunction {}; template  struct conjunction : std::integral_constant::value>{}; template  struct conjunction : std::integral_constant {}; template ::value...>::value>::type> auto foo_x(Args... args) -> void {} foo_x(x, x, y, d); // OK foo_x(x, x, y, d, z); // Error 

C ++ 14

Depuis C ++ 14, vous pouvez également utiliser le modèle de variable , la spécialisation partielle et static_assert . Par exemple:

 #include  template class, typename...> constexpr bool check = true; template class C, typename U, typename T, typename... O> constexpr bool check = C::value && check; template void f() { // use std::is_convertible or whichever is the best trait for your check static_assert(check, "!"); // ... } struct S {}; int main() { f(); // this won't work, for S is not convertible to int // f(); } 

Vous pouvez également utiliser conjointement avec std::enable_if_t comme type de retour, si vous ne souhaitez pas utiliser static_assert pour des raisons inconnues:

 template std::enable_if_t> f() { // ... } 

Etc…

C ++ 11

En C ++ 11, vous pouvez également concevoir une solution qui arrête immédiatement la récursivité lorsqu’un type non accepté est rencontré. Par exemple:

 #include  template struct check; template struct check: std::false_type {}; template struct check: check {}; template<> struct check<>: std::true_type {}; template void f() { // use std::is_convertible or whichever is the best trait for your check static_assert(check::value...>::value, "!"); // ... } struct S {}; int main() { f(); // this won't work, for S is not convertible to int // f(); } 

Comme mentionné ci-dessus, vous pouvez utiliser la check également dans le type de retour ou où vous voulez.

Qu’en est-il de la solution suivante?

— EDIT — Amélioration de la suggestion suivante de bolov et Jarod42 (merci!)

 #include  template  auto foo(Args... args) = delete; auto foo () { return 0; } template  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).

Vous l’avez déjà depuis le standard C ++ 11.

Un simple std::array (cas particulier de std::tuple où tous les éléments tuple partagent le même type) sera suffisant.

Toutefois, si vous souhaitez l’utiliser dans une fonction de modèle, vous pouvez mieux utiliser ´std :: initializer_list` comme dans l’exemple suivant:

 template< typename T > void foo( std::initializer_list elements ); 

Ceci est une solution très simple qui résout votre problème. L’utilisation des arguments de modèles variadiques est également une option, mais ajoute une complexité inutile à votre code. Rappelez-vous que votre code doit être lisible par les autres, y compris vous-même après un certain temps.

Que diriez- static_assert méthode du template static_assert et helper (solution c ++ 11):

 template  int assert_impl() { static_assert(b, "not convertable"); return 0; } template  void foo_x(Args... args) { int arr[] {assert_impl::value>()...}; (void)arr; } 

Un de plus c ++ 11 celui-ci utilise la solution basée sur “one-liner” sfinae:

 template {typename std::enable_if::value, int>::type{}...})> void foo_x(Args... args) { }