53

Can we restrict variadic template arguments to a certain type? I.e., achieve something like this (not real C++ of course):

struct X {};

auto foo(X... args)

Here my intention is to have a function which accepts a variable number of X parameters.

The closest we have is this:

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

but this accepts any type of parameter.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
bolov
  • 72,283
  • 15
  • 145
  • 224
  • With non-type template parameters, you can directly use the type. E.g. `int... args` – AndyG Sep 23 '16 at 11:04
  • 5
    `std::initializer_list` might be what you want as interface. – Jarod42 Sep 23 '16 at 11:25
  • @Jarod42 That's a way too. More of a workaround. Both have disadvantages. For example you can't move from `std::initializer_list`. And you have to use the `{p1, p2, p3}` syntax. With variadic templates the implementation is more complicated and you can easily screw up. That won't be true with concepts anymore as it gets very easy and clean with concepts. – bolov Sep 23 '16 at 11:28
  • @AndyG No, you can't. An expansion pattern must contain at least one template parameter pack. – L. F. Nov 02 '19 at 23:30
  • @L.F. That is a parameter pack. The `template<>` surrounding it was implied. [this](https://wandbox.org/permlink/GtPgsxyXX4qAWzYM) is what I meant. – AndyG Nov 03 '19 at 15:28
  • @AndyG the problem is that you cannot use runtime values – bolov Nov 03 '19 at 23:13
  • @AndyG Ah, now I see. Well, it’s true, but the use is more limited. – L. F. Nov 04 '19 at 09:00

6 Answers6

58

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
463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
bolov
  • 72,283
  • 15
  • 145
  • 224
  • 1
    The use of a fold expression for SFINAE is awesome. – AndyG Sep 23 '16 at 11:06
  • Did you have the concept arguments backwards? To From seems more right. – Yakk - Adam Nevraumont Sep 23 '16 at 11:15
  • @Yakk to me also To From seems more right mostly because To = From. But that't not how the standard does it: http://en.cppreference.com/w/cpp/types/is_convertible – bolov Sep 23 '16 at 11:17
  • 1
    Is this Q/A the right way to post a blog in stackoverflow? –  Sep 23 '16 at 11:19
  • 8
    @DieterLücking it is not a blog. And yes, it definitely is the right way: http://stackoverflow.com/help/self-answer . It is on topic. One clear and specific programming question. – bolov Sep 23 '16 at 11:22
  • 5
    @DieterLücking as long as Q/A are worth sharing like here I see no reason why not to... – W.F. Sep 23 '16 at 11:22
  • I mean `Convertible... Args` -- isn't `X` passed as first arg and `Args` as second to `Convertible`? But `Convertable` takes `From, To`, wrong order? – Yakk - Adam Nevraumont Sep 23 '16 at 13:06
  • @Yakk no. Arg is always the first param to the concept: `template A>` is the same as `template requires Binary_concept` – bolov Sep 23 '16 at 13:17
  • Ah, did not know that. Strange! But I guess it works. – Yakk - Adam Nevraumont Sep 23 '16 at 13:36
  • Now that Concepts are officially in C++20, you might want to update your answer. More specifically, you just need to remove `bool` and `constexpr` from the definition. – Rakete1111 Aug 28 '17 at 01:47
  • I have a similar problem to solve, though I want to accept **zero** **or** **more** arguments. introducing the following specialization of conjunction: `template <> struct conjunction<> : std::integral_constant {};` seems to do the trick. Are there unfortunate consequences from this? – ttyridal Feb 04 '18 at 17:45
  • @ttyridal it looks ok to me – bolov Feb 04 '18 at 19:34
7

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.

skypjack
  • 49,335
  • 19
  • 95
  • 187
  • Nice. Just a personal preference: I'd `s/typename C/typename Condition` or any other more meaningful name. Single letters are typically reserved for types, whereas callables et similia have more explaining names. Again, IMHO. – edmz Sep 23 '16 at 16:43
  • @black I agree with you (more or less), but I don't care usually about meaningful names for these kinds of short examples. ;-) – skypjack Sep 23 '16 at 16:44
  • That's true as well. :) – edmz Sep 23 '16 at 17:01
  • 1
    `all_of`/`check` can be implemented without recursion with `template struct bools{}; template struct all_of : std::is_same, bools> {};` – Jarod42 Sep 23 '16 at 18:30
  • @Jarod42 Chapeau. Nice trick this one. I wanted to show how to do that with variable template in C++14 (for no answer mentioned it), but I can use this pattern with the second example. Thank you. – skypjack Sep 23 '16 at 18:47
4

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

max66
  • 65,235
  • 10
  • 71
  • 111
  • 1
    That's a nice solution. But you can't always do that recursion. Or you don't want to. Might I sugest make `template auto foo(Args... args) = delete;` deleted. It's confusing otherwise. – bolov Sep 23 '16 at 11:48
  • @bolov - yes: the recursion is a limit; about `delete`, I've used it in my first attempt but work well only with my g++ (4.9.2); my clang++ (3.5) give me a lot of errors – max66 Sep 23 '16 at 11:53
  • 1
    You might prefer overload instead of specialization. – Jarod42 Sep 23 '16 at 13:46
  • @Jarod - I'm an idiod: I have to implement the overload before the specialization; thanks. – max66 Sep 23 '16 at 14:33
  • @bolog - `delete` didn't work because I'm an idiot; I've to define the base `foo()` as overload, non template (following Jarod42 suggestion) and do it *before* the specialization; thanks again. – max66 Sep 23 '16 at 14:36
1

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) {
}
W.F.
  • 13,888
  • 2
  • 34
  • 81
1

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.

Jorge Bellon
  • 2,901
  • 15
  • 25
0

The one aspect of this question that @463035818_is_not_a_number's answer does not address is how to avoid creating gratuitous copies of arguments. Here's an idea, due to this blog post, that minimizes copying arguments that are expensive to copy:

struct X { void use() { /* ... */ } /* ... */ };

template<typename Want, typename Have>
inline std::conditional_t<std::is_same_v<Want, Have>, Want &&, Want>
local_copy(Have &in)
{
  return static_cast<Have&&>(in);
}

template<std::convertible_to<X> ...T>
void
foo1(T&&...t)
{
  // Unary fold over comma operator
  (local_copy<X, T>(t).use(), ...);
}

// Another way to do it
template<std::convertible_to<X> ...T>
void
foo2(T&&...t)
{
  auto use = []<typename U>(U &&arg) {
    decltype(auto) x = local_copy<X, U>(arg);
    x.use();
  };
  (use(std::forward<T>(t)), ...);
}

The key idea is that local_copy will return a copy of its argument unless the argument was an rvalue of exactly the desired type, in which case it will return an rvalue reference to its argument. Hence, in the event that you call, say:

X x;
foo1(X{}, x);

The first argument, X{} will construct a temporary object before foo1's invocation, and foo1 can just modify this temporary in place and never copy it. By contrast, the second argument x gets copied, ensuring any modifications to the argument stay local to the function foo1.

user3188445
  • 4,062
  • 16
  • 26