2

I am looking for a way to implement Variadic function that accepts arguments of same type at compile-time and should be able to iterate on them. The variadic parameters are at the end with all of them having the same type. Something like below -

void SampleFunc(Other arguments(String may be)..., int... arg)
{
   for (const auto& val : arg)
   {
      // Each argument available here.
   }
}

then I will call this function like below -

SampleFunc("String", "{1,2,3,4})

Most important thing is that variadic parameters are hardcoded every time the function is called so I should be able to generate this Variadic argument at compile time.

Right now I am accepting function parameters as shown below -

void SampleFunc(std::string str, std::vector<int>& nums)

But this adds run time cost of constructing a vector every time function is called which I want to avoid.


UPDATE :-

I forgot to mention that this function has other parameters at the start. It is my bad, sorry about that. I have updated my example now.

JeJo
  • 30,635
  • 6
  • 49
  • 88
Anand Shah
  • 133
  • 10
  • To your update: you still can use the answer given below, just need to change the function signature: `template void SampleFunc(const atd::string& str, /* other arguments */ , Args&&... args)` – JeJo Aug 28 '20 at 06:58

3 Answers3

2

If the arguments are known at compile-time, in , using fold expressions you can do something like

#include <utility> // std::forward

template <typename Type>
void doSomething(Type&& arg) /* noexcept */
{
   // Each argument available here.
   std::cout << arg << "\n";
}

template <typename... Args> 
void SampleFunc(Args&&... args) /* noexcept */
{
   (doSomething(std::forward<Args>(args)), ...);
}

Now you could call the function like

SampleFunc(1, 2, 3, 4);   

and using doSomething you can do something with each arguments.

(See a Demo Online)


In previous compilers, you could imitate the fold expression via expander trick, as follows

template <typename Type>
void doSomething(Type&& arg) /* noexcept */
{
   // Each argument available here.
   std::cout << arg << "\n";
}

template <typename... Args> 
void SampleFunc(Args&&... args) /* noexcept */
{
   using dummy = int[];
   (void)dummy {
      0, (doSomething(std::forward<Args>(args)), 0)...
   };
}

(See a Demo Online)

JeJo
  • 30,635
  • 6
  • 49
  • 88
1

Iterate on variadic arguments is the simplest part: you tagged C++17 so you can use template folding, as suggested by JeJo, or other ways (recursion, initialization of an unused array).

More complicated is impose that all the arguments are exactly of the same type.

Obviously you can use SFINAE to impose that the deduced type are of the same type, but if you pass arguments of the different types, by example

foo(1l, 2l, 3l, 4); // long, long, long, int

when an argument is convertible to the type of the others, the code doesn't compile.

If you accept to pass through an additional function and that your function is a method of a template struct, you can start with an using that select the type from a couple type/index

template <typename T, std::size_t>
using get_type = T;

you can write the template struct as follows

template <typename...>
struct bar;

template <typename T, std::size_t ... Is>
struct bar<T, std::index_sequence<Is...>>
 {
   void operator() (std::string const & str, get_type<T, Is> const & ... ts)
    { ((std::cout << ts << ' '), ..., (std::cout << '\n')); }
 };

Observe that the arguments following str in the operator() are all of type T, where T is the first template argument of the struct.

The additional function is

template <typename T, typename ... Ts>
void foo (std::string const & str, Ts const & ... ts)
 { bar<T, std::index_sequence_for<Ts...>>{}(str, ts...); }

You can call foo() as follows

foo<int>("string", 1, 2, 3, 4l);

Observe that a long value (4l) is accepted because is converted to int.

You can also directly call the bar::operator(), if you prefer

bar<int, std::make_index_sequence<4u>>{}("string", 10, 20, 30, 40);

but you have to explicit the second template argument so there is some redundancies.

The following is a full compiling example

#include <string>
#include <utility>
#include <iostream>

template <typename T, std::size_t>
using get_type = T;

template <typename...>
struct bar;

template <typename T, std::size_t ... Is>
struct bar<T, std::index_sequence<Is...>>
 {
   void operator() (std::string const & str, get_type<T, Is> const & ... ts)
    { ((std::cout << ts << ' '), ..., (std::cout << '\n')); }
 };

template <typename T, typename ... Ts>
void foo (std::string const & str, Ts const & ... ts)
 { bar<T, std::index_sequence_for<Ts...>>{}(str, ts...); }

int main ()
 {
   foo<int>("string", 1, 2, 3, 4l); // a long value is converted to int

   bar<int, std::make_index_sequence<4u>>{}("string", 10, 20, 30, 40);
 }
max66
  • 65,235
  • 10
  • 71
  • 111
1

The variadic parameters are at the end with all of them having the same type.

Whereas std::vector might have the overhead of extra allocation, you might simply use std::initializer_list instead (of variadic).

void SampleFunc(std::string str, std::initializer_list<int>& nums)
{
    for (int val : nums)
    {
        // Each argument available here.
    }
}

With call similar to

SampleFunc("String", {1, 2, 3, 4});
Jarod42
  • 203,559
  • 14
  • 181
  • 302