- Modern C++ Programming Cookbook
- Marius Bancila
- 1396字
- 2021-06-11 18:22:19
Writing a function template with a variable number of arguments
It is sometimes useful to write functions with a variable number of arguments or classes with a variable number of members. Typical examples include functions such as printf, which takes a format and a variable number of arguments, or classes such as tuple. Before C++11, the former was possible only with the use of variadic macros (which enable writing only type-unsafe functions) and the latter was not possible at all. C++11 introduced variadic templates, which are templates with a variable number of arguments that make it possible to write both type-safe function templates with a variable number of arguments, and also class templates with a variable number of members. In this recipe, we will look at writing function templates.
Getting ready
Functions with a variable number of arguments are called variadic functions. Function templates with a variable number of arguments are called variadic function templates. Knowledge of C++ variadic macros (va_start, va_end, va_arg and va_copy, va_list) is not necessary for learning how to write variadic function templates, but it represents a good starting point.
We have already used variadic templates in our previous recipes, but this one will provide detailed explanations.
How to do it...
In order to write variadic function templates, you must perform the following steps:
- Define an overload with a fixed number of arguments to end compile-time recursion if the semantics of the variadic function template require it (refer to [1] in the following code).
- Define a template parameter pack that is a template parameter that can hold any number of arguments, including zero; these arguments can be either types, non-types, or templates (refer to [2]).
- Define a function parameter pack to hold any number of function arguments, including zero; the size of the template parameter pack and the corresponding function parameter pack is the same. This size can be determined with the sizeof... operator (refer to [3] and refer to the end of the How it works... section for information on this operator).
- Expand the parameter pack in order to replace it with the actual arguments being supplied (refer to [4]).
The following example, which illustrates all the preceding points, is a variadic function template that adds a variable number of arguments using operator+:
template <typename T> // [1] overload with fixed
T add(T value) // number of arguments
{
return value;
}
template <typename T, typename... Ts> // [2] typename... Ts
T add(T head, Ts... rest) // [3] Ts... rest
{
return head + add(rest...); // [4] rest...
}
How it works...
At first glance, the preceding implementation looks like recursion, because the function add() calls itself, and in a way it is, but it is a compile-time recursion that does not incur any sort of runtime recursion and overhead. The compiler actually generates several functions with a different number of arguments, based on the variadic function template's usage, so only function overloading is involved and not any sort of recursion. However, implementation is done as if parameters would be processed in a recursive manner with an end condition.
In the preceding code, we can identify the following key parts:
- Typename... Ts is a template parameter pack that indicates a variable number of template type arguments.
- Ts... rest is a function parameter pack that indicates a variable number of function arguments.
- rest... is an expansion of the function parameter pack.
The position of the ellipsis is not syntactically relevant. typename... Ts, typename ... Ts, and typename ...Ts are all equivalent.
In the add(T head, Ts... rest) parameter, head is the first element of the list of arguments, while ...rest is a pack with the rest of the parameters in the list (this can be zero or more). In the body of the function, rest... is an expansion of the function parameter pack. This means the compiler replaces the parameter pack with its elements in their order. In the add() function, we basically add the first argument to the sum of the remaining arguments, which gives the impression of recursive processing. This recursion ends when there is a single argument left, in which case the first add() overload (with a single argument) is called and returns the value of its argument.
This implementation of the function template add() enables us to write code, as shown here:
auto s1 = add(1, 2, 3, 4, 5);
// s1 = 15
auto s2 = add("hello"s, " "s, "world"s, "!"s);
// s2 = "hello world!"
When the compiler encounters add(1, 2, 3, 4, 5), it generates the following functions (arg1, arg2, and so on are not the actual names the compiler generates), which show that this is actually only calls to overloaded functions and not recursion:
int add(int head, int arg1, int arg2, int arg3, int arg4)
{return head + add(arg1, arg2, arg3, arg4);}
int add(int head, int arg1, int arg2, int arg3)
{return head + add(arg1, arg2, arg3);}
int add(int head, int arg1, int arg2)
{return head + add(arg1, arg2);}
int add(int head, int arg1)
{return head + add(arg1);}
int add(int value)
{return value;}
With GCC and Clang, you can use the __PRETTY_FUNCTION__ macro to print the name and the signature of the function.
By adding an std::cout << __PRETTY_FUNCTION__ << std::endl at the beginning of the two functions we wrote, we get the following when running the code:
T add(T, Ts ...) [with T = int; Ts = {int, int, int, int}]
T add(T, Ts ...) [with T = int; Ts = {int, int, int}]
T add(T, Ts ...) [with T = int; Ts = {int, int}]
T add(T, Ts ...) [with T = int; Ts = {int}]
T add(T) [with T = int]
Since this is a function template, it can be used with any type that supports operator+. The other example, add("hello"s, " "s, "world"s, "!"s), produces the hello world! string. However, the std::basic_string type has different overloads for operator+, including one that can concatenate a string into a character, so we should be able to also write the following:
auto s3 = add("hello"s, ' ', "world"s, '!');
// s3 = "hello world!"
However, that will generate compiler errors, as follows (note that I actually replaced std::basic_string<char, std::char_traits<char>, std::allocator<char> > with the string hello world! for simplicity):
In instantiation of 'T add(T, Ts ...) [with T = char; Ts = {string, char}]':
16:29: required from 'T add(T, Ts ...) [with T = string; Ts = {char, string, char}]'
22:46: required from here
16:29: error: cannot convert 'string' to 'char' in return
In function 'T add(T, Ts ...) [with T = char; Ts = {string, char}]':
17:1: warning: control reaches end of non-void function [-Wreturn-type]
What happens is that the compiler generates the code shown here, where the return type is the same as the type of the first argument. However, the first argument is either an std::string or a char (again, std::basic_string<char, std::char_traits<char>, std::allocator<char> > was replaced with string for simplicity). In cases where char is the type of the first argument, the type of the return value head+add (...), which is an std::string, does not match the function return type and does not have an implicit conversion to it:
string add(string head, char arg1, string arg2, char arg3)
{return head + add(arg1, arg2, arg3);}
char add(char head, string arg1, char arg2)
{return head + add(arg1, arg2);}
string add(string head, char arg1)
{return head + add(arg1);}
char add(char value)
{return value;}
We can fix this by modifying the variadic function template so that it has auto for the return type instead of T. In this case, the return type is always inferred from the return expression, and in our example, it will be std::string in all cases:
template <typename T, typename... Ts>
auto add(T head, Ts... rest)
{
return head + add(rest...);
}
It should be further added that a parameter pack can appear in a brace-initialization and that its size can be determined using the sizeof... operator. Also, variadic function templates do not necessarily imply compile-time recursion, as we have shown in this recipe. All these are shown in the following example:
template<typename... T>
auto make_even_tuple(T... a)
{
static_assert(sizeof...(a) % 2 == 0,
"expected an even number of arguments");
std::tuple<T...> t { a... };
return t;
}
auto t1 = make_even_tuple(1, 2, 3, 4); // OK
// error: expected an even number of arguments
auto t2 = make_even_tuple(1, 2, 3);
In the preceding snippet, we have defined a function that creates a tuple with an even number of members. We first use sizeof...(a) to make sure that we have an even number of arguments and assert by generating a compiler error otherwise. The sizeof... operator can be used with both template parameter packs and function parameter packs. sizeof...(a) and sizeof...(T) would produce the same value. Then, we create and return a tuple. The template parameter pack T is expanded (with T...) into the type arguments of the std::tuple class template, and the function parameter pack a is expanded (with a...) into the values for the tuple members using brace initialization.
See also
- Using fold expressions to simplify variadic function templates to learn how to write simpler and clearer code when creating function templates with a variable number of arguments
- Creating raw user-defined literals in Chapter 2, Working with Numbers and Strings, to understand how to provide a custom interpretation of an input sequence so that it changes the normal behavior of the compiler
- C++案例趣學(xué)
- Spring Cloud Alibaba核心技術(shù)與實(shí)戰(zhàn)案例
- Spring Cloud Alibaba微服務(wù)架構(gòu)設(shè)計與開發(fā)實(shí)戰(zhàn)
- 新一代通用視頻編碼H.266/VVC:原理、標(biāo)準(zhǔn)與實(shí)現(xiàn)
- Rust實(shí)戰(zhàn)
- Java面向?qū)ο蟪绦蜷_發(fā)及實(shí)戰(zhàn)
- Python面向?qū)ο缶幊蹋簶?gòu)建游戲和GUI
- 深入淺出PostgreSQL
- Android程序設(shè)計基礎(chǔ)
- 前端HTML+CSS修煉之道(視頻同步+直播)
- Java EE 8 Application Development
- Node.js Design Patterns
- Microsoft Azure Storage Essentials
- Visual Basic語言程序設(shè)計基礎(chǔ)(第3版)
- Building UIs with Wijmo