53 votes

Restreindre les arguments variadiques des modèles

Peut-on restreindre les arguments variadiques des modèles à un certain type ? Je veux dire, réaliser quelque chose comme ceci (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 X paramètres.

Le plus proche que nous ayons est celui-ci :

template <class... Args>
auto foo(Args... args)

mais celle-ci accepte tout type de paramètre.

0 votes

Avec les paramètres de modèle non typés, vous pouvez utiliser directement le type. Par exemple int... args

5 votes

std::initializer_list<X> pourrait être ce que vous voulez comme interface.

0 votes

@Jarod42 C'est aussi un moyen. C'est plutôt un moyen de contournement. Les deux ont des inconvénients. Par exemple, vous ne pouvez pas passer de std::initializer_list . Et vous devez utiliser le {p1, p2, p3} syntaxe. Avec les modèles variadiques, la mise en œuvre est plus compliquée et vous pouvez facilement vous tromper. Ce ne sera plus le cas avec les concepts, car l'implémentation est très simple et propre avec les concepts.

58voto

bolov Points 4005

Oui, c'est possible. Tout d'abord, vous devez décider si vous voulez accepter uniquement le type, ou si vous voulez accepter un type implicitement convertible. J'utilise std::is_convertible dans les exemples parce qu'il imite mieux le comportement des paramètres non modélisés, par exemple a long long acceptera un paramètre int argument. Si, pour une raison quelconque, vous avez besoin que seul ce type soit accepté, remplacez std::is_convertible con std:is_same (vous devrez peut-être ajouter std::remove_reference et std::remove_cv ).

Malheureusement, en C++ la conversion par rétrécissement, par exemple ( long long a int et même double a int ) sont des conversions implicites. Et alors que dans une configuration classique, vous pouvez obtenir des avertissements lorsque ces conversions se produisent, 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 faites une telle affectation. Mais avec une petite astuce, nous pouvons obtenir l'erreur à l'emplacement de l'appel avec les modèles aussi.

Alors, sans plus attendre, voici ce qui se passe :


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 du C++20

Pas encore ici, mais bientôt. Disponible dans gcc trunk (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. Surtout le

contraintes non satisfaites

c'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 beau expression des plis :

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 :

La déduction/substitution d'arguments de modèle a échoué : [...]

Rétrécissement

Nous pouvons éviter le rétrécissement, 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 une aide à la conjonction :
veuillez noter qu'en <code>C++17</code> il y aura un <code>std::conjunction</code> mais il faudra <code>std::integral_constant</code> arguments

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

juste des 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

1 votes

L'utilisation d'une expression de pli pour SFINAE est géniale.

0 votes

Aviez-vous les arguments du concept à l'envers ? À partir de semble plus juste.

0 votes

@Yakk pour moi aussi To From semble plus juste surtout parce que To = From. Mais ce n'est pas comme ça que la norme le fait : fr.cppreference.com/w/cpp/types/is_convertible

7voto

skypjack Points 5516

C++14

Depuis C++14, vous pouvez également utiliser modèle de variable la spécialisation partielle et static_assert pour le faire. A titre d'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 collaboration avec std::enable_if_t en tant que type de retour, si vous ne voulez pas utiliser static_assert pour des raisons inconnues :

template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
    // ...
}

Et ainsi de suite...

C++11

En C++11, vous pouvez également concevoir une solution qui arrête immédiatement la récursion lorsqu'un type qui ne doit pas être accepté est rencontré. À titre d'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 utiliser check également dans le type de retour ou où vous voulez.

0 votes

Joli. Juste une préférence personnelle : Je voudrais s/typename C/typename Condition ou tout autre nom plus significatif. Les lettres uniques sont généralement réservées aux types, alors que les callables et similia ont des noms plus explicites. Encore une fois, IMHO.

0 votes

@black Je suis d'accord avec vous (plus ou moins), mais je ne me soucie généralement pas de noms significatifs pour ce genre de courts exemples ;-)

0 votes

C'est vrai aussi. :)

4voto

max66 Points 4276

Qu'en est-il de la solution suivante ?

--- EDIT --- Amélioré suite à la suggestion 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) ne l'implémente que pour un seul type ( int dans cet exemple).

1 votes

C'est une bonne solution. Mais vous ne pouvez pas toujours faire cette récursion. Ou vous ne le voulez pas. Puis-je suggérer de faire template <class... Args> auto foo(Args... args) = delete; supprimé. C'est confus sinon.

0 votes

@bolov - oui : la récursion est une limite ; environ delete J'ai utilisé ce programme lors de ma première tentative mais il ne fonctionne bien qu'avec mon g++ (4.9.2) ; mon clang++ (3.5) me donne beaucoup d'erreurs.

1 votes

Vous pourriez préférer la surcharge à la spécialisation.

1voto

W.F. Points 3008

Et si static_assert et la méthode helper template (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 la solution "one-liner" basée sur 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) {
}

1voto

Jorge Bellón Points 1117

Vous l'avez déjà depuis la norme C++11.

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

Cependant, si vous voulez l'utiliser dans une fonction template, il vaut mieux utiliser une 'std::initializer_list` comme dans l'exemple suivant :

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

C'est une solution très simple qui résout votre problème. L'utilisation d'arguments variadiques pour les modèles est également une option, mais elle ajoute une complexité inutile à votre code. N'oubliez pas que votre code doit être lisible par d'autres, y compris par vous-même après un certain temps.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X