Restrict variadic template arguments

自古美人都是妖i 提交于 2019-11-27 11:02:12

Yes it is possible. First of all you need to decide if you want to accept only the type, or if you want to accept a implicitly convertible type. I use std::is_convertible in the examples because it better mimics the behavior of non-templated parameters, e.g. a long long parameter will accept an int argument. If for whatever reason you need just that type to be accepted, replace std::is_convertible with std:is_same (you might need to add std::remove_reference and std::remove_cv).

Unfortunately, in C++ narrowing conversion e.g. (long long to int and even duble to int) are implicit conversions. And while in a classical setup you can get warnings when those occur, you don't get that with std::is_convertible. At least not at the call. You might get the warnings in the body of the function if you make such an assignment. But with a little trick we can get the error at the call site with templates too.

So without further ado here it goes:


The testing rig:

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

Not here yet, but soon. This will be the most simple, clear and elegant solution

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:

We get a very nice error. Especially the

'Convertible' was not satisfied

is sweet:

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

Dealing with narrowing:

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

We make use of the very nice fold expression:

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

Unfortunately we get a less clear error:

template argument deduction/substitution failed: [...]

Narrowing

We can avoid narrowing, but we have to cook a trait is_convertible_no_narrowing (maybe name it differently):

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

We create a conjunction helper:
please note that in C++17 there will be a std::conjunction, but it will take std::integral_constant 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> {};

and now we can have our function:

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

just minor tweaks to the C++14 version:

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

C++14

Since C++14 you can use also variable template, partial specialization and static_assert to do that. As an example:

#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>();
}

You can also use check in conjunction with std::enable_if_t as a return type, if you don't want to use static_assert for some unknown reasons:

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

And so on...

C++11

In C++11, you can also design a solution that stops the recursion immediately when a type that is not to be accepted is encountered. As an example:

#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>();
}

As mentioned above, you can use check also in the return type or wherever you want.

What about the following solution ?

--- EDIT --- Improved following suggestion from bolov and Jarod42 (thanks!)

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

You can declare foo() to receive all types of arguments (Args ... args) but (recursively) implement it only for one type (int in this example).

You already have it since C++11 standard.

A simple std::array (special case of std::tuple where all the tuple elements share the same type) will be sufficient.

However, if you want to use it in a template function, you may better use an ´std::initializer_list` like in the following example:

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

This is a really simple solution which solves your problem. Using variadic template arguments is an option too, but adds unnecessary complexity to your code. Remember that your code should be readable by others, including yourself after some time.

How about static_assert and helper template method (c++11 solution):

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

One more c++11 this one uses "one-liner" sfinae-based solution:

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) {
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!