web-dev-qa-db-fra.com

Restreindre les arguments de modèle variadic

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.

41
bolov

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

Concepts C++ 20

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:

Faire face au rétrécissement:

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

C++ 17

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: [...]

Rétrécissement

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

C++ 14

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

C++ 11

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
47
bolov

C++ 14

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...

C++ 11

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.

6
skypjack

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).

4
max66

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) {
}
1
W.F.

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.

1
Jorge Bellon