Variadic template functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
 

void print()
{
    cout << "I am empty function and "
            "I am called at last.\n";
}

template <typename T, typename... Types>
void print(T var1, Types... var2)
{
    cout << var1 << endl;
 
    print(var2...);
}
 
// Driver code
int main()
{
    print(1, 2, 3.14,
          "Pass me any number of arguments",
          "I will print\n");
 
    return 0;
}


https://www.geeksforgeeks.org/variadic-function-templates-c/

Does anyone know WHY the C++ committee chose to do variable functions like this?

The calls are:
print(1, 2, 3.14,"Pass me any number of arguments", "I will print\n");
print(2, 3.14,"Pass me any number of arguments", "I will print\n");
print(3.14,"Pass me any number of arguments", "I will print\n");
print("Pass me any number of arguments", "I will print\n");

The last one has 2 parameters and they coincide with the template, which is great:
 
template <typename T, typename... Types>


BUT THEN, on the last call, there is only one parameter for the 2 parameter template:
print("I will print\n");

So that either they must add an empty/NULL parameter at the end of the call during compile time...something like this (similar to '\0' of char array):
print(1, 2, 3.14,"Pass me any number of arguments", "I will print\n", NULL);

or there must be some logic to assign empty/NULL inside the function for "Types... var2" at the end? For which the latter takes some more processing and resources, and affects speed.


Why couldn't they make variable functions behind the scene as simple as this, where the variadic calls itself and just stops at the last one? What was wrong with coding C++ behind the scenes to work this way?



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

//NON-functioning program!!!!!!!!!!!!!
template <typename... Types>
void print(Types... varN)
{
    cout << varN << endl;
}
 
// Driver code
int main()
{
    print(1, 2, 3.14,
          "Pass me any number of arguments",
          "I will print\n");
 
    return 0;
}



Last edited on
Does anyone know WHY the C++ committee chose to do variable functions like this?

The syntax might look a bit scary, especially if you're not used to writing templated code, but at least they are type-safe which is a big advantage compared to C-style variadic functions like printf.


BUT THEN, on the last call, there is only one parameter for the 2 parameter template:
print("I will print\n");
So that either they must add an empty/NULL parameter at the end of the call during compile time...something like this (similar to '\0' of char array):
...

Note that var2 is not a normal parameter. It is a parameter pack. It represents zero or more arguments.

When print("I will print\n"); is called then var1 will be of type const char* and point to the string "I will print\n". That string is then outputted on line 14. var2 is an empty parameter pack so when you expand the pack on line 16 there are no arguments passed to the function. It simply calls print(); which ends up calling the parameter-less print function above.


Why couldn't they make variable functions behind the scene as simple as this, where the variadic calls itself and just stops at the last one? What was wrong with coding C++ behind the scenes to work this way?
...

We might want to print the arguments in the opposite order. Sometimes we might not even want to do the same thing for each argument independently. We might want to compute a result that depends on all the arguments. Sometimes we just want to forward all the arguments to another function. These things would not be possible with what you propose.
Last edited on
Note that recursion and overloading is one technique to implement variadic function templates but there are other ways. You could for example do more advanced expansions using fold-expressions.

The following would accomplish the same result:
1
2
3
4
5
6
7
8
template <typename... Args>
void print(Args... args)
{
	((cout << args << endl), ...);
	
	cout << "I am empty function and "
	        "I am called at last.\n";
}

https://en.cppreference.com/w/cpp/language/fold
Last edited on
Oh ok, it lends itself to richer usage.

print(var2...);
is not called until the Var2... pack is empty.

At what point does the parameter pack peel itself, removing the left-most parameter in our case? After the end of the function, "}" or when the print function calls itself again?


ANOTHER PROGRAM:
What would the calls for this program be?

3x calls to : void Sum(Res& result, First first, Rest... valN)
Sum(dResult, 3.14, 4.56, 1.1111)
Sum(dResult, 4.56, 1.1111)
Sum(dResult, 1.1111)

But on this last call "Sum(dResult, 1.1111)". This still calls the 3 parameter Sum with an empty parameter pack and as a result function.......
return Sum(result, valN...);
........is called with 2 parameters? Where result has all the prior additions, including 1.1111 and "valN..." is empty?
Also, why is there a "return" there, when the function is "void Sum(..........)"




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

template <typename Res, typename ValType>
void Sum(Res& result, ValType& val)
{
	result = result + val;
}

template <typename Res, typename First, typename... Rest>
void Sum(Res& result, First first, Rest... valN)
{
	result = result + first;
	return Sum(result, valN...);
}


int main()
{
	double dResult = 0;
	Sum(dResult, 3.14, 4.56, 1.1111);
	cout << dResult << endl;

	string strResult;
	Sum(strResult, "Hello ", "World!");
	cout << strResult << endl;

	return 0;
}
The instantiation of templates (i.e. the creation of functions and classes from templates) happens at compile time. Pack expansion is part of that.

Your original program is essentially equivalent to:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
using namespace std;

void print()
{
    cout << "I am empty function and "
            "I am called at last.\n";
}

void print(const char* e)
{
    cout << e << endl;
 
    print();
}

void print(const char* d, const char* e)
{
    cout << d << endl;
 
    print(e);
}

void print(double c, const char* d, const char* e)
{
    cout << c << endl;
 
    print(d, e);
}

void print(int b, double c, const char* d, const char* e)
{
    cout << b << endl;
 
    print(c, d, e);
}

void print(int a, int b, double c, const char* d, const char* e)
{
    cout << a << endl;
 
    print(b, c, d, e);
}

int main()
{
    print(1, 2, 3.14,
          "Pass me any number of arguments",
          "I will print\n");
 
    return 0;
}
I just tried it without the return, and it works without it too:
return Sum(result, valN...);

The example shows with a return though, how does it even work with a return that has no return specified?

Good to know about fold-expressions. I took a brief look and it looks easy, but I don't want to confuse myself just yet.
ANOTHER PROGRAM:
...
But on this last call "Sum(dResult, 1.1111)". This still calls the 3 parameter Sum...

No. It calls the the first Sum, the one with two parameters.

Also, why is there a "return" there, when the function is "void Sum(..........)"

Probably a mistake. A leftover from an earlier attempt at making it return the sum rather than assigning it to the first argument. It doesn't do anything. Just remove it.
Last edited on
Oh no, that is not the way I thought it worked. I thought it was only 1 variadic function and that is somehow had a list or counter and then shifted each successive call one over and popped the left most off...in the SAME FUNCTION.

But instead, you are saying that the compiler sees variadic and then the unpacking = creating of all those recursive functions, so that all those functions are there at the same time. More elegant than what I thought it did, but all those calls I would imagine take longer and more resources than some list driven method.

I will have to re-evaluate the 2nd program in this new light. Thanks for showing me this, it was beautiful as no other video that I saw or read so far told it to me this way.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename Res, typename ValType>
void Sum(Res& result, ValType& val)
{
	result = result + val;
}

template <typename Res, typename First, typename... Rest>
void Sum(Res& result, First first, Rest... valN)
{
	result = result + first;
	return Sum(result, valN...);
}


int main()
{
	double dResult = 0;
	Sum(dResult, 3.14, 4.56, 1.1111);
	cout << dResult << endl;

	return 0;
}



I tried to write it out for the first variadic function template call in the last program:


void Sum(Res& result, double c) //Base call
{
result = result + c;
}

void Sum(Res& result, double b, double c)
{
result = result + b;
Sum(result, c);
}

void Sum(Res& result, double a, double b, double c)
{
result = result + a;
Sum(result, b, c);

}
More elegant than what I thought it did, but all those calls I would imagine take longer and more resources than some list driven method.

I wouldn't worry too much about it. Compilers are pretty good at optimizing and will often inline function calls.


I tried to write it out for the first variadic function template call in the last program:
...

It looks like you have understood how it works now. ️️😃
This can be slightly simplified by only adding to result once:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

template<typename Res>
void Sum(Res&) {}

template <typename Res, typename First, typename... Rest>
void Sum(Res& result, First first, Rest... valN) {
	result += first;
	Sum(result, valN...);
}

int main() {
	double dResult {};

	Sum(dResult, 3.14, 4.56, 1.1111);
	std::cout << dResult << '\n';
}



8.8111

Thanks.

void Sum(Res&) {}

And although this overloaded Sum does nothing, it is needed because valN... reaches zero at the end and needs to call a 1 parameter overloaded Sum function.


Sum(result); //valN... is zero.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Sum(Res&) {}

void Sum(Res& result, double c) 
{
	result += c;
	Sum(result);		//valN... is zero.
}

void Sum(Res& result, double b, double c) 
{
	result += b;
	Sum(result, double c);
}

void Sum(Res& result, double a, double b, double c) 
{
	result += a;
	Sum(result, double b, double c);
}
Topic archived. No new replies allowed.