Beware of Perfect Forwarding Constructors

The Problem

Suppose we want to write a wrapper class in modern C++. Of course we want to take advantage of move-semantics along with perfect forwarding. So let’s provide a move constructor as well as a templated constructor which perfectly forwards an argument to initialize our wrapped value. Something like:

template <typename T>
class wrapper {
  public:

  wrapper() = default;

  wrapper(const wrapper &/* that */) /* : value(that.value) */ {
    std::cout << "wrapper(const wrapper &)" << std::endl;
  }

  wrapper(wrapper &&/* that */) /* : value(std::move(that.value)) */ {
    std::cout << "wrapper(wrapper &&)" << std::endl;
  }

  template <typename U>
  wrapper(U &&/* u */) /* : value(std::forward<U>(u)) */ {
    std::cout << "wrapper(U &&)" << std::endl;
  }

  private:

  // T value;
};

NOTE: The ctor-init-lists are commented out because we want to analyze which constructors get invoked on various types, and it doesn’t compile in some of the cases we want to explore.

Let’s try passing wrapper by const &, &, const &&, and && with the intention to copy or move the object.

// const and non-const objs.
const wrapper<int> const_obj;
wrapper<int> non_const_obj;
// invoke constructors.
wrapper<int> const_lval(const_obj);
wrapper<int> non_const_lval(non_const_obj);
wrapper<int> const_rval(std::move(const_obj));
wrapper<int> non_const_rval(std::move(non_const_obj));

This outputs:

wrapper(const wrapper &)
wrapper(U &&)
wrapper(U &&)
wrapper(wrapper &&)

Huh? So passing wrapper by & and const && didn’t seem to invoke the correct constructors. Without the templated constructor, the output would be:

wrapper(const wrapper &)
wrapper(const wrapper &)
wrapper(const wrapper &)
wrapper(wrapper &&)

What’s going on here?

Explanation

It’s important to note that the parameter of the templated constructor is a Universal Reference. Without getting into too much detail, a universal reference binds to anything and preserves the qualifiers of the argument, that is, whether the argument is const, volatile, lvalue or rvalue. Let’s take a look at the second case, where we pass an instance of wrapper by &. Since universal reference preserves all the qualifiers, the templated constructor gets instantiated to wrapper(wrapper &).

NOTE: If you don’t understand how perfect forwarding works, Universal Reference by Scott Meyers illustrates how it all works. It boils down to a special rule for template type deduction in conjunction with reference collapsing, but that’s for another post.

Now overload resolution kicks in and selects between the two viable functions:

wrapper(const wrapper &); // copy constructor (user-defined / compiler-defined).
wrapper(wrapper &);       // instantiated templated constructor.

What we need to recognize is that wrapper & is an exact match for non_const_obj whereas the copy constructor requires an implicit conversion from wrapper & to const wrapper &. Since zero conversions is better than one, wrapper(wrapper &) wins overload resolution and thus the templated constructor gets invoked.

The same line of reasoning goes for the third case, where the template parameter gets instantiated to wrapper(const wrapper &&) and becomes an exact match for std::move(const_obj). For attempted move on const objects, we rely on the implicit conversion from const wrapper && to const wrapper & in order to fallback to a copy.

The bottom line of the problem is that templates can be instantiate to a better match than non-templates in some cases.

Solutions

Pass by Value

The preferred solution here is to forget about perfect forwarding and just go with the trivial pass-by-value-then-move pattern. As long as T is movable, we incur at most an extra move which is negligible in most cases anyway.

/* Pass-by-value constructor. */
wrapper(T value) : value(std::move(value)) {
  std::cout << "wrapper(T)" << std::endl;
}

NOTE: If T happens to be copyable but not movable, we incur an extra copy rather than an extra move. I was concerned with this situation but I feel better after a discussion with Sean Parent.

It’s important to note that alternative solutions should match this constructor in its behavior. I mean in regards to binding, rather than performance behavior.

Better Match

Another solution is to explicitly define the wrapper(wrapper &) constructor and delegate to the const version. But then while we’re at it we should also add another one for wrapper(const wrapper &&)… mm… not liking this already.

How does it compare to the model solution?

Suppose T is unrelated to wrapper, and that derived_wrapper is a class that publicly inherits from wrapper. If we attempt to copy-/move-construct off of a derived_wrapper, under the pass-by-value solution it would bind to the copy or move constructor. With this solution however, we would end up binding to the perfect forwarding constructor again.

The situation I described above is admittedly obscure. The point I’m trying to make here is that the templated constructor binds to anything, and attempting to hard-code the cases won’t work in general. A better approach is to constrain the templated constructor to be less greedy.

SFINAE

The last solution I’ll consider is to SFINAE out the templated constructor from the overload set. If you’ve read the standards document and have seen: “This constructor shall not participate in overload resolution.”, this is what it’s talking about. We enable the templated constructor only if U is convertible to T.

/* Constrained perfect forwarding constructor. */
template <
    typename U,
    std::enable_if_t<std::is_convertible<U, T>::value> * = nullptr>
wrapper(U &&u) : value(std::forward<U>(u)) {
  std::cout << "wrapper(U &&)" << std::endl;
}

Compared to the model solution?

This behaves equivalently to the pass-by-value pattern. For the pass-by-value pattern we have T as the parameter, which implies that we can call it with arguments that implicitly convertible to T. That semantic behavior is what we have captured here with the constraint condition.

EDIT: The solution is actually not equivalent to the pass-by-value pattern. Scott Meyers kindly pointed this out to me: “There are what I call ‘perfect forwarding failure cases’ that will succeed with pass-by-value but fail with perfect forwarding.”

Not New to C++11

It may seem like this is a new problem introduced in C++11, but it’s actually not a new problem. It’s just that it is much easier to encounter it because perfect-forwarding arguments is so tempting, whereas the C++98 way is perhaps less tempting.

In C++98 and also in C++11, the following template would also beat the copy constructor if a non-const lvalue is passed.

template <typename U>
wrapper(U &) {
  std::cout << "wrapper(U &)" << std::endl;
}

My point is that templates being instantiated to be better matching than non-templates is not a new problem. It’s not specific to perfect forwarding, nor is it specific to copy constructors. Therefore, it shouldn’t be remembered as if it’s a special case.

Summary

  • Avoid overloading templates and non-templates as the template could instantiateto be a better match (e.g. perfect-forwarding constructor) than the non-template which might rely on implicit conversions to handle all of its cases (e.g. copy constructor).
  • Prefer to use the pass-by-value-then-move pattern if possible. It’s much simpler, more readable, and only incurs at most an extra move as long as the type is movable.
  • If pass-by-value-then-move is not an option, (e.g. variadic templates, efficiency) then consider the constraints that the pass-by-value version would impose and satisfy those requirements with template metaprogramming techniques such as tag dispatch, SFINAE and explicit/partial specializations.

Credits

The pitfalls of perfect-forwarding constructors have been discussed by others already.