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 {
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.