63

Question is simple, how would I implement a function taking a variable number of arguments (alike the variadic template), however where all arguments have the same type, say int.

I was thinking about something alike this;

void func(int... Arguments)

Alternatively wont a recursive static assert on the types work?

8
  • 3
    If you need a variable number of int arguments, why not pass a vector to it? What will these arguments do? Commented Aug 2, 2013 at 13:02
  • Oh >.<, I really just could do that. And I'm just playing around with variadic templates. Commented Aug 2, 2013 at 13:04
  • 1
    Don't use "...". The ellipsis is a dangerous carryover from C. Use higher-level C++ constructs and libraries instead. ("C++ Coding Standards", Sutter/Alexandrescu) Commented Aug 2, 2013 at 13:17
  • 16
    @DanielDaranas, OP is referring to parameter packs, not the C-style varargs ellipsis. Commented Aug 2, 2013 at 13:23
  • 4
    @Shark: 1. So as not to use the stack. 2. So as not to depend on <vector> 3. To be more flexible. 4. To be less flexible, i.e. not have someone pass a zillion elements in a vector. 5. To be constexpr. Commented Dec 9, 2016 at 19:44

7 Answers 7

58

A possible solution is to make the parameter type a container that can be initialized by a brace initializer list, such as std::initializer_list<int> or std::vector<int>. For example:

#include <iostream>
#include <initializer_list>

void func(std::initializer_list<int> a_args)
{
    for (auto i: a_args) std::cout << i << '\n';
}

int main()
{
    func({4, 7});
    func({4, 7, 12, 14});
}
Sign up to request clarification or add additional context in comments.

6 Comments

Say the numbers aren't constants will this approach still work?
Thats great but the number of arguments here is known at runtime only. initializer_list lacks a constexpr version. I would need that :(
This works as long as the function doesnt take variable number of references. std::initializer_list<int&> doesn't work
There is a design flaw with std::initialiser_list in that it's not move-aware. For this reason, when passing large or non-copyable types you should prefer Jonathan Wakely's solution (below) that allows perfect forwarding.
this passes those values {4,7} by value right? even if they were named int variables?
|
23

Here's a version that removes the function from the overload set, instead of giving a static_assert. This is allows you to provide other overloads of the function that could be used when the types aren't all the same, rather than a fatal static_assert that can't be avoided.

#include <type_traits>

template<typename... T>
  struct all_same : std::false_type { };

template<>
  struct all_same<> : std::true_type { };

template<typename T>
  struct all_same<T> : std::true_type { };

template<typename T, typename... Ts>
  struct all_same<T, T, Ts...> : all_same<T, Ts...> { };

template<typename... T>
typename std::enable_if<all_same<T...>::value, void>::type
func(T...)
{ }

If you want to support perfect forwarding you probably want to decay the types before checking them, so that the function will accept a mix of lvalue and rvalue arguments as long as they have the same type:

template<typename... T>
typename std::enable_if<all_same<typename std::decay<T>::type...>::value, void>::type
func(T&&...)
{ }

Alternatively, if you have a general purpose trait for testing the logical conjunction you can do it using std::is_same instead of writing your own all_same:

template<typename T, typename... Ts>
typename std::enable_if<and_<is_same<T, Ts>...>::value, void>::type
func(T&&, Ts&&...)
{ }

Because this requires at least one argument you'd also need another overload to support the zero-argument case:

void func() { }

The and_ helper can be defined like so:

template<typename...>
  struct and_;

template<>
  struct and_<>
  : public std::true_type
  { };

template<typename B1>
  struct and_<B1>
  : public B1
  { };

template<typename B1, typename B2>
  struct and_<B1, B2>
  : public std::conditional<B1::value, B2, B1>::type
  { };

template<typename B1, typename B2, typename B3, typename... Bn>
  struct and_<B1, B2, B3, Bn...>
  : public std::conditional<B1::value, and_<B2, B3, Bn...>, B1>::type
  { };

1 Comment

This is unfortunately not 100% the same as the putative func(Foo... foos), because if Foo has a non-explicit constructor, func({x, y, z}) won't work -- the compiler can't infer the type of {x, y, z} unless that type is specified in the signature of func.
14

I think you can do this by specifying a concrete type when chewing your arguments out of the argument pack. Something like:

class MyClass{};
class MyOtherClass{};

void func()
{
    // do something
}

template< typename... Arguments >
void func( MyClass arg, Arguments ... args )
{
    // do something with arg
    func( args... );
    // do something more with arg
}


void main()
{
    MyClass a, b, c;
    MyOtherClass d;
    int i;
    float f;

    func( a, b, c );    // compiles fine
    func( i, f, d );    // cannot convert
}

In the generic case void func( MyClass arg, Arguments ... args ) would become void func( arg, Arguments ... args ) with a template type T.

Comments

11

@Skeen How about this?

template <typename T>
void func_1(std::initializer_list<T>&& a) {
    // do something
} 

template <typename... T>
void func(T&&... a) {
    func_1({std::forward<T>(a)...});
} 

int main() {
    func(1, 2, 3);
    // func(1, 2, 3, 4.0); // OK doesn't compile
}

1 Comment

@Barry have you tried? I did at tutorialspoint.com/codingground.htm both literal int and int& are decayed on function call.
6

If you don't want to use brace-based initializer_list/vector and want to keep the arguments separate in form of argument pack, then below solution checks it at compile time using recursive static_asserts:

#include<type_traits>

template<typename T1, typename T2, typename... Error>
struct is_same : std::false_type {};

template<typename T, typename... Checking>
struct is_same<T, T, Checking...> : is_same<T, Checking...> {}; 

template<typename T>
struct is_same<T,T> : std::true_type {};

template<typename... LeftMost>
void func (LeftMost&&... args)
{
  static_assert(is_same<typename std::decay<LeftMost>::type...>::value, 
                "All types are not same as 'LeftMost'");
  // ...
}

int main ()
{
  int var = 2;
  func(1,var,3,4,5);  // ok
  func(1,2,3,4.0,5); // error due to `static_assert` failure
}

Actually this solution would check all the arguments with respect to the first argument. Suppose it was double then everything would be checked against double.

1 Comment

Using a forwarding reference (LeftMost&&) means that the static assertion will fail if you call the function with a mix of lvalues and rvalues, e.g. int i=0; func(i, 1); so it would be better to check is_same<typename std::decay<LeftMost>::type...> instead.
2

Because I don't think I saw this solution, you could write a specific function for every type (in your case, just int) then a forwarding function taking variadic argument types.

Write each specific case:

then for each specific case:

// only int in your case
void func(int i){
    std::cout << "int i = " << i << std::endl;
}

Then your forwarding function like this:

template<typename Arg0, typename Arg1 typename ... Args>
void func(Arg0 &&arg0, Arg1 &&arg1, Args &&... args){
    func(std::forward<Arg0>(arg0));
    func(std::forward<Arg1>(arg1), std::forward<Args>(args)...);
}

This is good because it is expandable for when you want to accept maybe another type too.

Used like this:

int main(){
    func(1, 2, 3, 4); // works fine
    func(1.0f, 2.0f, 3.0f, 4.0f); // compile error, no func(float)
}

Comments

0

I believe even in C++26 this isn't directly supported. It was proposed for C++23 but was not accepted.

However, as a workaround you can do something like this:

#include <utility>
#include <iostream>
#include <array>

namespace detail {
  template<typename T>
  using MakeInt = int;

  template<typename... AllInt>
  void func_impl(AllInt... args)
  {
    // Whatever you want your function to do.
    // Here were just printing the arguments
    // as an example.
    std::array<int, sizeof...(args)> arr{args...};
    const char* sep = "";
    for (int x : arr) {
      std::cout << sep << x;
      sep = ", ";
    }
    std::cout << std::endl;
  }
}

template<typename... Args>
auto func(Args&&... args)
  -> decltype(detail::func_impl<detail::MakeInt<Args>...>(std::forward<Args>(args)...))
{
    return detail::func_impl<detail::MakeInt<Args>...>(std::forward<Args>(args)...);
}

int main() {
  func(0, 1, 2, 3);
  // It even works with types other than int,
  // since they get implicitly converted to
  // int when calling func_impl.
  func('\0', 1u, 2.0, 3.0f);
}

Output:

0, 1, 2, 3
0, 1, 2, 3

The basic idea is that once we know how many arguments are being passed in, we can just call a generic template function (func_impl) while explicitly specifying the argument types as int, int, ..., int for however many arguments we have. To do this automatically, we define func as a forwarding wrapper around func_impl that forwards the arguments and passed the template argument list as int, int, ..., int.

The fact that you can pass in arguments that aren't exactly ints, and they get implicitly converted to int, makes this very similar to a regular function that takes a number of ints as arguments.

However it isn't perfect, for example you can't do func({}) because func can't deduce the type of the braces, whereas if you have a function void g(int) you would be able to call g({}) because the complier already knows that the expected type is int.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.