59

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?

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
Skeen
  • 4,614
  • 5
  • 41
  • 67
  • 3
    If you need a variable number of int arguments, why not pass a vector to it? What will these arguments do? – Shark Aug 02 '13 at 13:02
  • Oh >.<, I really just could do that. And I'm just playing around with variadic templates. – Skeen Aug 02 '13 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) – Daniel Daranas Aug 02 '13 at 13:17
  • 16
    @DanielDaranas, OP is referring to parameter packs, not the C-style varargs ellipsis. – dhavenith Aug 02 '13 at 13:23
  • @dhavenith Ah, yes. I get it now. – Daniel Daranas Aug 02 '13 at 13:40
  • 4
    @Shark: 1. So as not to use the stack. 2. So as not to depend on `` 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. – einpoklum Dec 09 '16 at 19:44
  • Does this answer your question? [Variable number of parameters in function in C++](https://stackoverflow.com/questions/1579719/variable-number-of-parameters-in-function-in-c) – xskxzr Aug 04 '21 at 03:27

6 Answers6

59

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});
}
hmjd
  • 120,187
  • 20
  • 207
  • 252
  • Say the numbers aren't constants will this approach still work? – Skeen Aug 02 '13 at 13:15
  • 1
    Thats great but the number of arguments here is known at runtime only. initializer_list lacks a constexpr version. I would need that :( – Jean-Bernard Jansen Jun 25 '14 at 23:57
  • 1
    This works as long as the function doesnt take variable number of references. `std::initializer_list` doesn't work – balki Nov 22 '14 at 02:50
  • 1
    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. – Richard Hodges Jul 03 '15 at 08:35
  • this passes those values `{4,7}` by value right? even if they were named int variables? – jiggunjer Jul 04 '15 at 12:44
  • @hmjd: And will this take anything and cast it intoan initializer list? e.g. a tuple, a vector, anything? – einpoklum Dec 09 '16 at 19:45
21

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
  { };
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 2
    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`. – Justin L. Feb 04 '18 at 18:52
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.

user2746401
  • 3,157
  • 2
  • 21
  • 46
10

@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
}
dusketha
  • 101
  • 1
  • 3
  • @Barry have you tried? I did at http://www.tutorialspoint.com/codingground.htm both literal int and int& are decayed on function call. – dusketha Jul 16 '15 at 15:38
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.

iammilind
  • 68,093
  • 33
  • 169
  • 336
  • 3
    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::type...>` instead. – Jonathan Wakely Jul 02 '15 at 13:37
  • @JonathanWakely, agree. Thanks! – iammilind Jul 03 '15 at 04:01
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)
}
RamblingMad
  • 5,332
  • 2
  • 24
  • 48