-1

I've seen a snippet this code from this answer and I can't seem to understand how the arrangement of templates and typenames create the final function call that is shown in main().

In particular, the arrangement of variadic template templates and the use of std::forward are confusing to me.

Can anybody break this snippet of code down so I can better understand it?

#include <utility>

template <template <typename...> class TemplateClass, typename... Args>
TemplateClass<Args...> make(Args&&... args)
{
    return TemplateClass<Args...>(std::forward<Args>(args)...);
}

int main() 
{
  make<std::pair>(1, 2);
}

Any help would be greatly appreciated.

Community
  • 1
  • 1
puradox
  • 1,358
  • 1
  • 12
  • 13
  • Related: http://stackoverflow.com/q/3582001 – dyp Sep 22 '14 at 07:59
  • 1
    If you remove all the `...` does it make more sense? Sometimes it is easier to understand these examples is to remove the "variadic" part and then add them back (they add detail to what needs to be expanded and where). – Niall Sep 22 '14 at 08:00

2 Answers2

7

This is a function template with a template template-parameter:

template <template <typename, typename> class TT>
TT<int, double>
make(int i, double d)
{
    return TT<int, double>(i, d);
}

You specify a class or alias template which has two type parameters, and this function will use a specialization of this template, created using int and double as the template arguments. For example:

make< std::pair >(42, 4.2);

This returns a std::pair<int, double>.


We can now "templatize" the (function) arguments of this function:

template <template <typename, typename> class TT, typename Arg0, typename Arg1>
TT<Arg0, Arg1>
make(Arg0 a0, Arg1 a1)
{
    return TT<A0, A1>(a0, a1);
}

The template parameters Arg0 and Arg1 are intended to be deduced from the types of the (function) arguments used to call the function:

int i = 42;
double d = 4.2;
make< std::pair >(i, d); // returns a `std::pair<int, double>`

For more complex data types, we might want to use perfect forwarding:

make< std::pair >( 42, std::vector<int>(1000) );

This returns a std::pair<int, std::vector<int>>. The above definition of make will move the temporary vector created via std::vector<int>(1000) into the second function parameter. However, it will be copied from there into the object created via TT<A0, A1>(a0, a1).

We can change the definition of make to implement perfect forwarding as follows:

template <template <typename, typename> class TT, typename Arg0, typename Arg1>
TT<Arg0, Arg1>
make(Arg0&& a0, Arg1&& a1)
{
    return TT<A0, A1>(std::forward<A0>(a0), std::forward<A1>(a1));
}

The temporary vector created via std::vector<int>(1000) will be moved into the object created within the return-statement.


Now, we can generalize this function template to N arguments; the template template-parameter also has to be generalized so that you can pass any class or alias template that takes some number of type parameters.

template <template <typename...> class TemplateClass, typename... Args>
TemplateClass<Args...> make(Args&&... args)
{
    return TemplateClass<Args...>(std::forward<Args>(args)...);
}
dyp
  • 38,334
  • 13
  • 112
  • 177
2

Line by line breakdown of the code

template <template <typename...> class TemplateClass, typename... Args>

The TemplateClass is itself a template that requires a variable template argument. Without the variadic part, this could look like template <typename> class TemplateClass for a single argument, or template <typename, typename> class TemplateClass for two arguments. Args are the variadic template arguments to this function template make.

TemplateClass<Args...> make(Args&&... args)

The return is of type TemplateClass<Args...>, from the original template argument to the template function, and is defined using the variadic Args of the original template. make takes a variadic parameter pack called args by what is often called a "universal reference" (the &&, this may or may not eventually be a rvalue reference in this case).

{
    return TemplateClass<Args...>(std::forward<Args>(args)...);
}

An object of the return type is created using a constructor that accepts all the argument originally provided to the function.

Perfecting forwarding is used to ensure that the value category is maintained for each argument originally received. The ... expands what is to the left of it, so for each argument in the parameter pack, a std::forward is applied (which is what is required since std::forward only accepts a single argument.

How does perfect forwarding work?

In a nutshell, the combination of reference collapsing and a static_cast. Basically if the argument is a lvalue reference, the return type to the std::forward is also a lvalue reference. If it is an rvalue reference, then the return type is a rvalue reference (akin to the result of a std::move).

How do "universal references" (or "forwarding references") work?

They power the mechanics of std::forward and similar functions, like the make in this case. Another example is the std::make_shared function in the standard library. They are so named because they can bind to "any" reference (both & and &&) because of the reference collapsing rules introduced in C++11.

Community
  • 1
  • 1
Niall
  • 30,036
  • 10
  • 99
  • 142