📜 ⬆️ ⬇️

Argument numbering of the variadic template, or what the modest pair hides



Mastering the C ++ 11 standard is a process that cannot be abrupt. Studying a new language construct requires not only memorizing the syntax, but also understanding its purpose and typical methods of application. An important tool in training is a prettier STL, which can often open its eyes to the existence of very interesting and necessary features. And knowing that some thing is possible and implemented in the STL, getting to the method of implementation is not difficult.

One of the curious examples associated with the updated and improved class pair , and will be discussed in the article.
The new standard has added such a seemingly simple thing as a pair , convenience and versatility. If earlier, the types that were part of the pair were subject to rather severe requirements, now you can make almost anything into a pair. In particular, the restriction on the design of such types has been removed. Now it is not necessary to use copy or even move operations, it is possible to create a pair by directly constructing members (such an operation is called emplace, "placement", and in C ++ 11 it is supported by STL containers), using non-trivial constructors.

... And here, as they say, more. How can we call the pair constructor and pass it two sets of arguments, so that he understands which arguments to give to which constructor? Among the usual designers associated with copying or moving members or the whole pair , we see this:
')
template< class... Args1, class... Args2 > pair( std::piecewise_construct_t, std::tuple<Args1...> first_args, std::tuple<Args2...> second_args ); 


piecewise_construct_t is just an empty type that will help us signal that we want to create a pair piece by piece, passing arguments to the first and second constructors. In this we will be helped by a constant of this type called piecewise_construct . Well, then we specify two sets of arguments, wrapping them in tuples ( tuple , the make_tuple function will help in their creation). For those readers who have forgotten or do not know what it is, I recall: a tuple is a collection of an arbitrary number of values ​​of an arbitrary type. In C ++, with its strict type control, tuples are implemented using templates with a variable number of arguments ( variadic template ).

Well, the problem seems to have been successfully solved: the tuples act as “packages” of arguments for the first and second constructors. At this stage, a programmer who is familiar with the standard's novelties may ask: “By the way, how do you unpack data from tuples?” The documentation gives us the only way: the get function, which specifies the element index in the tuple as a template parameter.

How do our arguments get into the constructor? Retrieving data from a tuple one at a time is easy. It's easy to retrieve them recursively. But how to fit all the values ​​in the function call (in this case, the constructor)?
Here is useful unpacking variadic-templates. However, you need to unpack not the list of tuple types, but the list of indices. Which first you need to make more.

Let's start with the main thing: let's add a new index to the existing list. Obviously, if the numbering starts from 0, then the new index will be equal to the size of the input list. We need a structure that we parameterize with a list of indices:

 template<size_t ... Indices> struct PackIndices { //     Indices  ,  sizeof ... (Indices) }; 


The result, which is a list of integer compile-time constants and at the same time template arguments, cannot be stored by itself, but the type parameterized by these arguments can be stored. And we just have a suitable candidate for this type of role:

 template<size_t ... Indices> struct PackIndices { typedef PackIndices<Indices... , sizeof ... (Indices)> next; }; 


Now we will make a recursive generator that creates a list of indices of length N. This is done by simply adding the last index to the list with a length of N-1 :

 template<size_t N> struct CreatePackIndices { typedef typename CreatePackIndices<N-1>::type::next type; }; 


... and stop the recursion:

 template<> struct CreatePackIndices<0> { typedef PackIndices<> type; }; 


Having gotten a way to create a list of indexes, let's unpack the tuple into the constructor's parameters. For simplicity, consider first the construction of only one object, first .

Using the args tuple and the Indices index list, unpacking should look basically like this:

 first(std::get<Indices>(args)...) 


To access the Indices , it is necessary that the context in which we decompress (that is, the pair constructor) is parameterized by this list. This means that we will need to create a second template constructor with all the necessary parameters. Here, by the way, there will be another new C ++ 11, delegation of constructors, which allows you to call an alternative constructor in the initialization list. And since we still make calls to functions, we will use automatic output of the type of the arguments: pass an anonymous PackIndices object to the auxiliary constructor. As a result, we get such a one-legged pair :

 template<typename T> class pair { // ,   template<typename ... ArgTypes, size_t ... Indices> pair(std::tuple<ArgTypes...>& first_args, PackIndices<Indices...>): first(std::get<Indices>(first_args)...) {} public: // ,   template<typename ... ArgTypes> pair(std::piecewise_construct_t, const std::tuple<ArgTypes...>& first_args): pair(first_args, typename CreatePackIndices<sizeof ... (ArgTypes)>::type()) {} private: T first; }; 


This is the time to recall perfect forwarding - the mechanism necessary for correctly passing arguments to nested calls without changing their types. The updated STL provides a forward function, which will have to be applied to each argument and, in addition, parameterized by the type of the argument. Fortunately, the creators of the new standard provided for such a clever thing as the simultaneous unpacking of several sets of arguments. Since ArgTypes and Indices obviously have the same length, you can safely add the call forward to the decompression pattern:

 template<typename ... ArgTypes, size_t ... Indices> pair(std::tuple<ArgTypes...>& first_args, PackIndices<Indices...>): first(std::forward<ArgTypes>(std::get<Indices>(first_args))...) {} 


After going all the way from the values ​​in make_tuple to the parameters of the constructor, put the pair on both legs:
Code
 template<typename T1, typename T2> class pair { // ,   template<typename ... ArgTypes1, size_t ... Indices1, typename ... ArgTypes2, size_t ... Indices2> pair(std::tuple<ArgTypes1...>& first_args, std::tuple<ArgTypes2...>& second_args, PackIndices<Indices1...>, PackIndices<Indices2...>): first(std::forward<ArgTypes1>(std::get<Indices1>(first_args))...), second(std::forward<ArgTypes2>(std::get<Indices2>(second_args))...) {} public: // ,   template<typename ... ArgTypes1, typename ... ArgTypes2> pair(std::piecewise_construct_t, std::tuple<ArgTypes1...> first_args, std::tuple<ArgTypes2...> second_args): pair(first_args, second_args, typename CreatePackIndices<sizeof ... (ArgTypes1)>::type(), typename CreatePackIndices<sizeof ... (ArgTypes2)>::type()) {} private: T1 first; T2 second; }; 


Of course, this technique is useful not only for creating a handicraft and cycling pair . So, a programmer dealing with stack-based virtual machines will probably come to mind for functions. Undoubtedly, there are applications in other areas.

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


All Articles