官术网_书友最值得收藏!

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:

  1. 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).
  2. 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]).
  3. 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).
  4. 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
主站蜘蛛池模板: 本溪市| 辉县市| 南漳县| 北宁市| 鹰潭市| 绵阳市| 玛沁县| 吕梁市| 辽源市| 丹东市| 呼图壁县| 苏尼特左旗| 贡嘎县| 罗平县| 新干县| 公主岭市| 东源县| 汤阴县| 灯塔市| 民乐县| 潼南县| 泌阳县| 萝北县| 丰台区| 盐山县| 株洲县| 阿拉善右旗| 拉萨市| 崇阳县| 开江县| 黑龙江省| 酒泉市| 博罗县| 曲周县| 贺州市| 大同县| 桐柏县| 哈巴河县| 明溪县| 陇南市| 定襄县|