C++17 introduces class template deduction: a way for the compiler to deduce the arguments to construct a class template without our having to write a make_* function. But it’s not quite as straightforward as it seems.

Imagine we have a simple type that will tell us when it’s copied or moved, just for testing.

struct S { S ( ) = default ; S ( const S & ) { std :: cout << "copy

" ; } S ( S && ) { std :: cout << "move

" ; } } ; struct S { S() = default; S(const S&) { std::cout << "copy

"; } S(S&&) { std::cout << "move

"; } };

And likewise a very simple template like so:

template < typename T > struct Foo { Foo ( const T & t_ ) : t ( t_ ) { } Foo ( T && t_ ) : t ( std :: move ( t_ ) ) { } private : T t ; } ; template <typename T> struct Foo { Foo(const T& t_) : t(t_) {} Foo(T&& t_) : t(std::move(t_)) {} private: T t; };

Note that we provide two forms of constructor to deal with both rvalues and lvalues being passed in. With C++14, prior to class template deduction, we would write a make_foo function to instantiate this template something like this:

template < typename T > auto make_foo ( T && t ) { return Foo < std :: decay_t < T >> ( std :: forward < T > ( t ) ) ; } template <typename T> auto make_foo(T&& t) { return Foo<std::decay_t<T>>(std::forward<T>(t)); }

And call it like so:

int main ( ) { // rvalue: move auto f1 = make_foo ( S ( ) ) ; // lvalue: copy S s ; auto f2 = make_foo ( s ) ; } int main() { // rvalue: move auto f1 = make_foo(S()); // lvalue: copy S s; auto f2 = make_foo(s); }

The important thing here is that the template argument to make_foo is deduced, and the template argument to Foo is not (cannot be, in C++14). Furthermore, because the template argument to make_foo is deduced, make_foo ‘s argument is a forwarding reference rather than an rvalue reference, and hence we use perfect forwarding to pass it along to the appropriate constructor for Foo .

So far so good. Now, with C++17, class template deduction is available to us, so we can get rid of make_foo . Now our main() function looks like this:

int main ( ) { // rvalue: move? Foo f3 { S ( ) } ; // lvalue: copy? S s ; Foo f4 { s } ; } int main() { // rvalue: move? Foo f3{S()}; // lvalue: copy? S s; Foo f4{s}; }

Here’s the unintuitive part: the template arguments are being deduced now. But in that case, doesn’t that mean that Foo ‘s constructor argument is being deduced? Which means that what looks like Foo ‘s rvalue constructor is actually now a constructor with a forwarding reference that will outcompete the other constructor? If so, that would be bad – we could end up moving from an lvalue!

I woke up the other morning with this worrying thought and had to investigate. The good news is that even though the code looks like it would break, it doesn’t.

So in fact, yes, although Foo ‘s template argument is being deduced, I think the crucial thing is that Foo ‘s constructor still takes an rvalue reference – not a forwarding reference – at the point of declaration. And according to 16.3.1.8 [over.match.class.deduct] the compiler is forming an overload set of function templates that match the signatures of the constructors available, and it’s using the correct types. In other words, I think it’s doing something that we could not do: it’s forming a function template, for the purposes of deduction, whose argument is an rvalue reference rather than a forwarding reference.

As is often the case in C++, one needs to be careful to properly distinguish things. It is very easy to get confused over rvalue references and forwarding references, because they look the same. The difference is that forwarding references must be deduced… and in this case, even though it looks like it’s deduced, it isn’t.

Edit: Reddit user mps1729 points out that indeed, neither of the implicitly generated functions is using a forwarding reference, as clarified in 17.8.2.1/3 [temp.deduct.call]. Thanks for the clarification!

Share this: Facebook

Twitter

Reddit

Email

