Restrict Variadic Template Arguments

Restrict variadic template arguments

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

Acceptable way to restrict variadic templates in constructors

Overload resolution doesn't consider accessibility; that check is done later.

What you are doing is not actually SFINAE; it is not affecting overload resolution, only making the instantiation of the template constructor ill-formed after it's selected by overload resolution.

We can see this by adding a non-template constructor that's not as good a match:

template< class T >
class myobject
{
/* ... */
public:

/* ... */
myobject( const char * ptr )
{
std::cout << "const char * constructor" << std::endl;
}

template< typename... Ts >
myobject( Ts... params )
: myobject( typename CheckVaradicArgs<Ts...>::type(), params... )
{
std::cout << "Ts constructor with " << sizeof...(params) << std::endl;
}

};

char * p = nullptr;
myobject<int> something(p);

If it were SFINAE, the template would be taken out of overload resolution, and the const char * constructor would be selected. Instead, you get a compile error as the compiler tries to instantiate the variadic template (g++ does the same thing).

To SFINAE, just add an extra template parameter:

template< class T >
class myobject
{

template< typename... Ts >
using CheckVariadicArgs =
typename std::enable_if<
all_true< std::is_convertible<Ts, T>... >::value, int
>::type;
public:

myobject( const myobject& other )
{
std::cout << "Copy constructor" << std::endl;
}

template< typename... Ts, CheckVariadicArgs<Ts...>* = nullptr >
myobject( Ts... params )
{
std::cout << "Ts constructor with " << sizeof...(params) << std::endl;
}

};

Demo.

Limit the number of parameters in a variadic template parameter pack

To make the function not callable when there's too many arguments, you can constraint the function with sfinae. That way, if there's another overload that accepts more arguments, the compiler will be able to select the correct overload.

A simple std::enable_if with the condition will suffice:

template <class ...Args, std::enable_if_t<(sizeof...(Args) <= 10)>* = nullptr>
void setRequestArguments(const Args&... args)
{
const std::vector<QGenericArgument> vec = {args... };
}

For the sake of readability, you can put the constraint in the trailing return type of your function:

template <class ...Args>
auto setRequestArguments(const Args&... args) -> std::enable_if_t<(sizeof...(args) <= 10)>
{
const std::vector<QGenericArgument> vec = {args... };
}

Here's an updated version for C++20 using requires and terse template syntax:

auto setRequestArguments(const auto&... args) requires (sizeof...(args) <= 10) -> void {
const std::vector<QGenericArgument> vec = {args... };
}

C++ templates that accept only certain types

I suggest using Boost's static assert feature in concert with is_base_of from the Boost Type Traits library:

template<typename T>
class ObservableList {
BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
...
};

In some other, simpler cases, you can simply forward-declare a global template, but only define (explicitly or partially specialise) it for the valid types:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
...
};

// All other types are invalid, and will cause linker error messages.

[Minor EDIT 6/12/2013: Using a declared-but-not-defined template will result in linker, not compiler, error messages.]

Variadic template partial specialization of a class to restrict type of template arguments

This works:

#include <iostream>

template <int i, typename T> struct Arg;

template <typename ...T>
class Foo;

template <int ...Is, typename ...Ts>
class Foo<Arg<Is, Ts>...>
{
public:
static constexpr unsigned int N = sizeof...(Is);
};

int main()
{
using type2 = Foo<Arg<1, int>, Arg<7, float>, Arg<1, int>>;
std::cout << type2::N << "\n";
}

Though it might or might not be easy or convenient to use the template arguments in that form, depending on what you want to do with them.

C++ variadic template - limit number of args

Yes, this is possible, without static_assert. For example, let's assume we want our vector class to only be assignable with the name number of arguments as the dimension of the vector, you can use:

template<std::size_t Dim>
struct vector {
template <typename X0, typename ...Xn>
typename std::enable_if<sizeof...(Xn) + 1 == Dim, void>::type
assign(X0 x0, Xn... xn) {}
};

This just uses std::enable_if in combination with sizeof... to enable or disable the specific assign function.

So the following will compile:

vector<3> x;
x.assign(1, 2, 3);

Live demo

but this won't:

vector<3> x;
x.assign(1, 2, 3, 4);

Live demo

with (for Clang):

main.cpp:14:7: error: no matching member function for call to 'assign'
x.assign(1, 2, 3, 4);
~~^~~~~~

and neither will this:

vector<3> x;
x.assign(1, 2);

Live demo

with a similar error message.

How to iterate over Variadic template types (not arguments)?

In your example, in C++17, you might do:

template<typename... Ts>
class Subscriber
{
Subscriber()
{
auto f = [](auto data){ /* do something with data*/ };
(PubSub.Subscribe<Ts>(f), ...);
}
}

In C++11/14, you might to use more verbose way, such as:

(C++14 currently with your generic lambda)

template<typename... Ts>
class Subscriber
{
Subscriber()
{
auto f = [](auto data){ /* do something with data*/ };
int dummy[] = {0, (PubSub.Subscribe<Ts>(f), void(), 0)...};
static_cast<void>(dummy); // Avoid warning for unused variable.
}
}


Related Topics



Leave a reply



Submit