📜 ⬆️ ⬇️

Variadic templates. Tuples, unpacking and more

In this post I will talk about templates with a variable number of parameters. As an example, the simplest implementation of the tuple class will be given. I will also talk about unpacking tuple 'a and substitutions, the values ​​stored there as function arguments. And finally, I will give an example of using the techniques described above to implement deferred function execution, which can be used, for example, as an analogue of finally blocks in other languages.

Theory


A template with a variable number of parameters ( variadic template ) is a template of a function or class that accepts the so-called parameter pack . When declaring a template, it looks like this.

template<typename… Args> struct some_type; 

Such a record means that a template can take 0 or more types as its arguments. In the body of the same template, the syntax for using is slightly different.

 template<typename… Args> //  void foo(Args… args); //  

The call foo (1,2.3, “abcd”) is instantiated into foo <int, double, const char *> (1, 2.3, “abcd”) . The parameter pack has many interesting properties (for example, they can be used in lambda capture sheets or in brace-init-lists ), but now I would like to focus on two properties that I will actively use further on.
')
1. A variadic parameter can be used as an argument of a function call, apply caste operations to it, etc. At the same time, it is revealed depending on the position of the ellipsis, namely, the expression directly adjacent to the ellipsis is revealed. It sounds incomprehensible, but with an example I think everything will become clear.

 template<typename T> T bar(T t) {/*...*/} template<typename... Args> void foo(Args... args) { //... } template<typename... Args> void foo2(Args... args) { foo(bar(args)...); } 


In this example, in the foo2 function, since the ellipse is after calling bar () , then first for each value from args the function bar () is called, and the values ​​returned by foo () will fall into bar () .
Some more examples.

 (const args&..) // -> (const T1& arg1, const T2& arg2, ...) ((f(args) + g(args))...) // -> (f(arg1) + g(arg1), f(arg2) + g(arg2), ...) (f(args...) + g(args...)) // -> (f(arg1, arg2,...) + g(arg1, arg2, ...)) (std::make_tuple(std::forward<Args>(args)...)) // -> (std::make_tuple(std::forward<T1>(arg1), std::forward<T2>(arg2), ...)) 


2. The number of parameters in the pack can be obtained using the sizeof operator ...

 template<typename... Args> void foo(Args... args) { std::cout << sizeof...(args) << std::endl; } foo(1, 2.3) // 2 


Tuple


The Tuple class is interesting, as it seems to me, not so much so that the variadic templates (you can do without them) are used to write and create auxiliary functions, but rather because the tuple is a recursive data structure, a newcomer from another functional world (hi Haskell), which in turn once again shows how versatile C ++ can be.
I will give you a simplest implementation of this class sketched out on the knee, which, nevertheless, shows the basic technique of working with variadic templates - “biting off the head” of a package of parameters and recursive processing of the “tail”, which, by the way, is also widely distributed in functional languages.
So.
The base class template is never instantiated, therefore without a body.

  template<typename... Args> struct tuple; 


The main specialization of the template. Here we separate the “head” of the parameter types and the “head” of the arguments passed to us in the constructor. We keep this argument in the current class, the rest will be recursively taken up by the base ones. We can access the base class data to access the base class data.

  template<typename Head, typename... Tail> struct tuple<Head, Tail...> : tuple<Tail...> { tuple(Head h, Tail... tail) : tuple<Tail...>(tail...), head_(h) {} typedef tuple<Tail...> base_type; typedef Head value_type; base_type& base = static_cast<base_type&>(*this); Head head_; }; 


The final touch (as usual in functional languages) is to specialize the bottom of the recursion.

  template<> struct tuple<> {}; 


In general, the required minimum has already been written. You can use our class as follows:

  tuple<int, double, int> t(12, 2.34, 89); std::cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl; 


However, manually counting how many times it is necessary to write .base to get to the element we need is not very convenient, so the standard library has a get () function template that allows you to get the contents of the Nth element of the object of the tuple class. We are forced to wrap a function in a structure in order to circumvent the ban on partial specialization of functions. In this basic pattern, there is also a “biting of the head” from tupl and redirection to the next type getter with an index value one less than in the case of the type of the element, and, in fact, the function of obtaining this element.

  template<int I, typename Head, typename... Args> struct getter { typedef typename getter<I-1, Args...>::return_type return_type; static return_type get(tuple<Head, Args...> t) { return getter<I-1, Args...>::get(t); } }; 


And only when we hit the bottom of recursion, can the first real actions be done. The type of the return value, we take this time from stupid and return the same value taken from there.

  template<typename Head, typename... Args> struct getter<0, Head, Args...> { typedef typename tuple<Head, Args...>::value_type return_type; static return_type get(tuple<Head, Args...> t) { return t.head_; } }; 


Well, as is usually the case, a small helper function is written, which saves us from having to manually write the parameters of the structure template.

  template<int I, typename Head, typename... Args> typename getter<I, Head, Args...>::return_type get(tuple<Head, Args...> t) { return getter<I, Head, Args...>::get(t); } 


We use this function.

  test::tuple<int, double, int> t(12, 2.34, 89); std::cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl; std::cout << get<0>(t) << " " << get<1>(t) << “ “ << get<2>(t) << std::endl; 


Unpacking


Unpacking a tuple in C ++! What could be cooler =)? This feature seemed so important to the creators of Python that they even added a special syntax to the language to support this operation. Now we can use it in C ++. This can be implemented in different ways (at least outwardly, the principle itself is the same everywhere), but I will show here the simplest solution in my opinion. In addition, it resembles what we saw above in the implementation of getter 'a to extract stupid elements. Property number 1, described in the theory above, will help us here. Our decompression function should look something like this.

 template<typename F, typename Tuple, int… N> auto call(F f, Tuple&& t) { return f(std::get<N>(std::forward<Tuple>(t))...); } 


As you remember,
 f(std::get<N>(std::forward<Tuple>(t))...); 

unpacked in
  f(std::get<N1>(std::forward<Tuple>(t)), std::get<N2>(std::forward<Tuple>(t)), ...) 


But there is one problem, namely, in such a function you will need to manually specify all the template template arguments, and specify them correctly (in the right order and the right amount). It would be very good if we could automate this process. To do this, we proceed in a manner similar to the approach to extracting elements from a blunt way.

 template<typename F, typename Tuple, bool Enough, int TotalArgs, int... N> struct call_impl { auto static call(F f, Tuple&& t) { return call_impl<F, Tuple, TotalArgs == 1 + sizeof...(N), TotalArgs, N..., sizeof...(N) >::call(f, std::forward<Tuple>(t)); } }; 


Here, I think, it is worth explaining in more detail. Let's start with the template parameters. With F and Tuple, I think everything is clear. The first one is responsible for our callable object, the second one, in fact, for tuple, from which we will take objects and slip callable 'as arguments for the call. Next comes the boolean parameter Enough . It signals whether enough int parameters were typed in ... N and we will further specialize our template. Finally, TotalArgs - a value equal to the size of a stupid In the call function, as before, we redirect the call to the next instantiation of the template recursively.
At the same time in the very first call the type will be
 call_impl<F, Tuple, TotalArgs == 1, TotalArgs, 0> // (N… - , sizeof...(N) = 0) 
in the second
 call_impl<F, Tuple, TotalArgs == 2, TotalArgs, 0, 1> // (N… =0, sizeof...(N) = 1) 
etc. that is exactly what we need.

Finally, we need specialization, in which real actions will be performed, our function will finally be called with the necessary arguments. This specialization is as follows

 template<typename F, typename Tuple, int TotalArgs, int... N> struct call_impl<F, Tuple, true, TotalArgs, N...> { auto static call(F f, Tuple&& t) { return f(std::get<N>(std::forward<Tuple>(t))...); } }; 


Also auxiliary function will not interfere.

 template<typename F, typename Tuple> auto call(F f, Tuple&& t) { typedef typename std::decay<Tuple>::type type; return call_impl<F, Tuple, 0 == std::tuple_size<type>::value, std::tuple_size<type>::value >::call(f, std::forward<Tuple>(t)); } 


Here, I think, everything is transparent.
You can use it as follows.

 int foo(int i, double d) { std::cout << "foo: " << i << " " << d << std::endl; return i; } std::tuple<int, double> t1(1, 2.3); std::cout << call(foo, t1) << std::endl; 


Defer


The above techniques allow you to organize lazy, pending calculations. As a particular example of such calculations, I will consider here the situation when you need to perform some kind of functional, no matter how we exit the function, regardless of the conditional constructions inside, and whether the exception was caused. This behavior is similar to finally blocks in pythons and Java or, for example, in the Go language there is a defer operator that provides the behavior described above.
I want to make a reservation right away that, like many other things in C ++, this task can be solved in various ways, for example, using std :: bind or lambda, which collects arguments and returns another lambda, etc. But also storing the callable object and stupid with the necessary arguments is quite appropriate.
Actually, knowing what we already know, the implementation is trivial.

 template<typename F, typename... Args> struct defer { defer(F f, Args&&... args) : f_(f), args_(std::make_tuple(std::forward<Args>(args)...)) {} F f_; std::tuple<Args...> args_; ~defer() { try { call(f_, args_); } catch(...) {} } }; 


As usual, the helper function

 template<typename F, typename... Args> defer<F, Args...> make_deferred(F f, Args&&... args) { return defer<F, Args...>(f, std::forward<Args>(args)...); } 


And use

 auto d = make_deferred(foo, 1 ,2); 

Source: https://habr.com/ru/post/228031/


All Articles