Restrict variadic template arguments
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.
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 double
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
};
C++20 Concepts
Not here yet, but soon. Available in gcc trunk (March 2020). This is the most simple, clear, elegant and safe solution:
#include <concepts>
auto foo(std::convertible_to<X> auto ... args) {}
foo(x, x, y, d); // OK
foo(x, x, y, d, z); // error:
We get a very nice error. Especially the
constraints not satisfied
is sweet.
Dealing with narrowing:
I didn't find a concept in the library so we need to create one:
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
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