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 and substituting the values ​​stored there as arguments to the function. And finally, I will give an example of using the above techniques 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 struct some_type;
    

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

    template // Объявление
    void foo(Args… args); // Использование
    

    A call to foo (1,2.3, “abcd”) is instantiated in foo(1, 2.3, “abcd”). In parameter pack 's has many interesting properties (eg they can be used in sheet capturing lambdas or brace-the init-lists ), but now I would like to stay at the two properties, which I will continue to actively use.

    1. A variadic parameter can be used as an argument to a function call, cast operations can be applied to it, etc. At the same time, it is revealed depending on the position of the ellipsis, namely, the expression immediately adjacent to the ellipsis is revealed. It sounds incomprehensible, but with an example I think everything will become clear.

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


    In this example, in the function foo2 , since the ellipsis is after the call to bar () , first the bar () function is called for each value from args , and the values ​​returned by bar () get into foo () as arguments . A few 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)...)) // -> (std::make_tuple(std::forward(arg1), std::forward(arg2), ...))
    


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

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


    Tuple


    The Tuple class is interesting, it seems to me, not so much because variadic templates are now used to write it and create auxiliary functions (you can do without them), but because tuple is a recursive data structure, an alien from another, functional world (hello Haskell), which in turn once again shows how versatile C ++ can be.
    I will give a simple implementation of such a class, sketched on the knee, which, nevertheless, shows the basic technique for working with variadic templates - “biting off the head” of the parameter pack and recursive processing of the “tail”, which, by the way, is also widespread in functional languages.
    So.
    The base class template is never instantiated, therefore without a body.

        template
        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 save this argument in the current class, the rest will be recursively taken by the base ones. We can access the data of the base class by casting “ourselves” to the base type.

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


    The final touch (which is again familiar to functional languages) is to specialize the “bottom” of recursion.

        template<>
        struct tuple<>
        {};
    


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

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


    However, it’s not very convenient to manually count how many times you need to write .base to get to the element you need, so the get () function template is written in the standard library , which allows you to get the contents of the Nth element of an 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 template, there is also “biting the head” from the blunt and redirecting to the next getter type with the index value one less both in the case of the element type and, in fact, the function of obtaining this element.

        template
        struct getter
        {
            typedef typename getter::return_type return_type;
            static return_type get(tuple t)
            {
                return getter::get(t);
            }
        };
    


    And only when we come to the bottom of the recursion can the first real actions be done. We will take the type of the return value this time from the dull and return the value taken from there.

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


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

        template
        typename getter::return_type
        get(tuple t)
        {
            return getter::get(t);
        }
    


    We use this function.

        test::tuple 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 tuple in C ++! What could be cooler =)? This feature seemed so important to the creators of Python that they even introduced special syntax into the language to support this operation. Now we can use it in C ++. It can be implemented in different ways (at least externally, 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 when implementing getter 'a to retrieve elements of a dummy. Here, property number 1, described in the theory above, will help us. Our unpack function should look something like this

    template
    auto call(F f, Tuple&& t)
    {
    	return f(std::get(std::forward(t))...);
    }
    


    As you remember,
    f(std::get(std::forward(t))...);
    

    unpack in
     f(std::get(std::forward(t)), std::get(std::forward(t)), ...) 
    


    But there is one problem, namely, in such a function, it will be necessary to manually specify all the int arguments of the template, 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 will act in a similar way to the extraction of elements from a stupid way.

    template
    struct call_impl
    {
        auto static call(F f, Tuple&& t)
        {
            return call_impl::call(f, std::forward(t));
        }
    };
    


    Here, it seems to me, it is worth explaining in more detail. Let's start with the template options. With F and Tuple, I think everything is clear. The first is responsible for our callable object, the second, in fact, for the tupl, from which we will take objects and palm off callable 'y as arguments to the call. Next is the boolean parameter Enough . It signals whether enough int parameters have already accumulated in ... N and by it we will further specialize our template. Finally, TotalArgs - a value equal to the size of the dull. In the call function, we, as before, redirect the call recursively to the next instantiation of the template.
    In this case, in the very first call, the type will be
    call_impl // (N… - пусто, sizeof...(N) = 0)
    
    in the second
    call_impl  // (N… =0, sizeof...(N) = 1)
    
    etc. that is exactly what we need.

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

    template
    struct call_impl
    {
        auto static call(F f, Tuple&& t)
        {
            return f(std::get(std::forward(t))...);
        }
    };
    


    Also, an auxiliary function does not hurt.

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


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

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


    Defer


    The techniques described above allow you to organize lazy, pending calculations. As a private example of such calculations, I will consider a situation where you need to perform some kind of functionality, regardless of how we exit the function, regardless of conditional constructions inside, or whether an exception was raised. This behavior is similar to finally blocks in python and Java, or, for example, in Go, there is a defer statement that provides the behavior described above.
    I want to immediately make a reservation that like many other things in C ++, this problem can be solved in various ways, for example, using std :: bind or a lambda that collects arguments and returns another lambda, etc. But also storage of callable object and is stupid with the necessary arguments quite suitable .
    Actually, knowing what we already know, the implementation is trivial.

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


    As usual, an auxiliary function

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


    And use

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

    Also popular now: