At the most recent NWCPP meeting in Redmond, WA, the always-entertaining Scott Meyers shared his latest insights about so-called “universal references” and their pitfalls. In particular, he was warning about the hazards of overloading on universal references. His advice was good, I thought, but missed some important corner cases about the interactions between universal references and copy constructors. In this article, I show what the special problems are and some ways to avoid them.

Universal References

But first, a refresher. What does Scott mean by “universal references”? He basically means this:

template<typename T> void foo( T && t ) { // "T &&" is a UNIVERSAL REFERENCE }

In the above code, T && is what Scott calls a universal reference. In C++, there are lvalue references and rvalue references to distinguish between references to named and unnamed objects (roughly). The rules of template type deduction and reference collapsing conspire to make the above simple syntax have the seemingly magical property that T && can bind to anything, rvalue or lvalue. Let me repeat that, with emphasis: T && here can be either an lvalue reference or an rvalue reference. Consider:

int i = 42; foo( i ); // lvalue, "T &&" deduced to be "int &" foo( 42 ); // rvalue, "T &&" deduced to be "int &&"

See? foo can be called with either lvalues or rvalues, and the deduced type of T && reflects that. (Perfect forwarding relies on that property of universal references.) It’s that somewhat magical property that led Scott to coin the phrase “universal references.”

Avoid Overloading on Universal References

Scott’s advice is simple and sound: avoid overloading on universal references. By which he means, don’t do this:

template<typename T> void foo( T const & t ) {/*...*/} template<typename T> void foo( T && t ) {/*...*/}

In the code above, the author presumably wanted all lvalues to go to the first and all rvalues to go to the second. But that’s not what happens. What happens is this: const lvalues most certainly go to the first, and all rvalues most certainly go to the second, but non-const lvalues also go to the second. See, the second overload takes a universal reference, which, as you recall, binds to anything. As we saw above, T && could deduce to int & . If we pass a non-const integer, the second overload which can take an int & is a better match than the first which can take an int const & at best.

Sadly, this is not some esoteric problem you can safely forget about. I’ve seen people make this mistake in the real world, and in one case, the code was accidentally moving from an lvalue as a result, leaving a ticking time bomb in production code.

Scott’s advice is to instead write one function, the one taking the universal reference, and internally dispatch to one of two helpers. One sensible way to dispatch might be to use the std::is_lvalue_reference trait, like so:

template<typename T> void foo_impl( T && t, std::true_type ) {/* LVALUES HERE */} template<typename T> void foo_impl( T && t, std::false_type ) {/* RVALUES HERE */} template<typename T> void foo( T && t ) { foo_impl( std::forward<T>(t), std::is_lvalue_reference<T>() ); }

Although verbose, I agree that this is a fairly straightforward way of handling this particular problem.

Special Problems with the Special Member Functions

This is all well and good. We can chalk this up as Yet Another C++ Quirk, learn to recognize the quicksand and avoid stepping in it. If only we could get off so easily! The problem comes from the copy constructor. C++ has rules for when it gets generated automatically. Ordinarily that’s a boon that saves users from typing repetitive boilerplate, but sometimes it can be surprising.

Consider a simple wrapper object that holds some object of type T :

template<typename T> struct wrapper { T value; wrapper( T const & v ) : value( v ) {} };

That’s dandy. But this is 2013 and we have move semantics and perfect forwarding now, so we want to change our wrapper to take advantage of them. To get perfect forwarding, we have to use universal references, so we do this:

template<typename T> struct wrapper { T value; template<typename U> wrapper( U && u ) : value( std::forward<U>(u) ) {} }; // The array is perfectly forwarded to the // string constructor. wrapper<std::string> str("hello world");

This is kosher, right? Sadly not, because in some circumstances, the compiler will try to use the above constructor as a copy constructor, and that’s not good.

But wait! you say. A template can’t be used as a copy constructor! If that’s what you’re thinking, you’re almost right. The truth is — and Scott Meyers correctly points this out — that the compiler refuses to use a template to generate a copy constructor. The difference is subtle but crucially important, as we’ll see.

When the compiler sees this:

// Copy the wrapper wrapper<std::string> str2 = str;

… it looks at the wrapper class and, seeing no copy constructor (and refusing to use the template to generate one), it automatically generates a new one:

template<typename T> struct wrapper { T value; template<typename U> wrapper( U && u ) : value( std::forward<U>(u) ) {} // THIS IS COMPILER-GENERATED: wrapper( wrapper const & that ) : value( that.value ) {} };

What happens next is truly bizarre. The compiler, after generating a constructor to use, then decides not to use it. Say what?! That’s right. Overload resolution now kicks in. Recall that the code of interest is:

wrapper<std::string> str2 = str;

str is a non-const lvalue of type wrapper<std::string> . There are two constructors to choose from. The compiler-generated one is certainly viable, but the first is a better match. Why? Because U && can be deduced as wrapper<std::string> & . Although a template is never used to generate a copy constructor, a template may end up being used anyway if overload resolution selects it. In short, we end up forwarding a wrapper object to the std::string constructor, and we fail. Oops. Had str had been const, then the other constructor would have been selected and it would have worked. Schitzo!

Variadic templates are another fly in this ointment. Consider the following:

template<typename ... Ts> struct tuple { // Whoops, this can be a copy constructor! template<typename ... Us> tuple( Us &&... us ) : /* etc... */ };

The intent here is to define a tuple type with a constructor that perfectly forwards all its argument. And it can be used that way, but (hold on to your hats) it can also be used as a copy constructor! In that case, Us &&... deduces to tuple & . Whoa.

The Solution

So what’s a well-intentioned C++ programmer to do? What if you really, really want a constructor that perfectly forwards one argument? There are a bunch of “fixes,” but most have their own problems. Here is what I’ve found to work the most reliably.

// write this once and put it somewhere you can // reuse it template<typename A, typename B> using disable_if_same_or_derived = typename std::enable_if< !std::is_base_of<A,typename std::remove_reference<B>::type >::value >::type; template<typename T> struct wrapper { T value; template<typename U, typename X = disable_if_same_or_derived<wrapper,U>> wrapper( U && u ) : value( std::forward<U>(u) ) {} };

There’s a lot going on there, but the gist of it is this: we use metaprogramming to disable the constructor if the parameter is a wrapper . In fact, the constructor is disabled for types derived from wrapper , too. Why? Because it preserves the expected semantics of C++. Consider:

struct A {}; struct B : A {}; B b; A a = b;

There’s nothing wrong with doing that. B inherits from A , so we can construct an A from a B and we get slicing behavior. If A were to acquire one of these troublesome universal constructors we’ve been discussing, it would no longer slice. The universal constructor would get called instead, and we’d get some new, exciting, and probably wrong behavior.

Summary

In short, take Scott’s advice and don’t overload on universal references. But if you are writing a universal constructor (that is, a single-argument constructor that takes a universal reference), constrain the template so that it can’t be used as a copy constructor. You’ll be sorry if you don’t!