Universal References and the Copy Constructor

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!

25 thoughts on “Universal References and the Copy Constructor

    • I didn’t forget to specify the template parameter to wrapper here. Within the definition of a class template, you can refer to the current instantiation without specifying the template parameters. So in this context, wrapper and wrapper<T> are synonyms.

        • I didn’t, although I think I caused some confusion by changing the code after I posted it. Originally, I used a class template called disable_if_is_base_of, and it needed the ::type and typename as you suggest. But based on feedback, and on reflection, I decided a template alias would be better, and I renamed it to disable_if_same_or_derived. In this formulation, no ::type or typename is needed.

          • Could you please post your original class template disable_if_is_base_of? Visual Studio 2012 C++ compiler doesn’t support template aliases.

  1. I think the problem is that the whole endeavour of perfect forwarding is ambiguous.

    Consider


    struct foo
    {
    foo();
    foo(wrapper<foo> const&);
    };

    int main()
    {
    wrapper<foo> a;
    wrapper<foo> b(a); // call foo::foo(foo const&)
    // or foo::foo(wrapper<foo> const&) ?
    }

    • I don’t see how it’s ambiguous. The default copy constructor does member-wise copy. So wrapper‘s copy constructor copies the foo member via its copy constructor. Anything else would be surprising.

  2. Not to spam, but instead of enable_if, one could choose the traits + static_assert route:

    #include <string>
    #include <type_traits>

    template<typename T>
    class wrapper
    {
    public:
    T value;

    template<typename U>
    wrapper(U && u) : wrapper(
    std::forward<U>(u),
    std::is_same<
    wrapper,
    typename std::remove_reference<U>::type
    >()
    ) {}

    private:
    template<typename U>
    wrapper(U && u, std::true_type)
    {
    static_assert(
    // Avoid premature failure due to first phase lookup
    std::is_same<U, std::false_type>::value,
    "No copying!"
    );
    }

    template<typename U>
    wrapper(U && u, std::false_type) : value(std::forward<U>(u))
    {
    }
    };

    int main()
    {
    wrapper<std::string> a{"hi"};
    auto b = a; // No copying!
    }

  3. @Bartosz Bielecki: I don’t think you want to use std::is_same, because std::is_same takes cv-qualifiers into account. That means that std::is_same<wrapper, const wrapper>::value is 1, which is not what you want. You can use std::is_base_of, as Eric did, because that ignores cv-qualifiers. (It also handles inheritance, as Eric pointed out.) Or you can use std::decay instead of std::remove_reference on U, because std::decay removes both cv- and ref-qualifiers.

    • Thanks, Scott. Don’t you mean std::is_same<wrapper, const wrapper>::value is false? Also, I personally find using std::decay for this distasteful because it really means something else. It converts function types and arrays to pointers in addition to stripping top-level reference and cv-qualifiers (although that obviously can’t have any effect here). Still, I would define my own remove_cvref trait and use that instead.

      • @Eric: Yes, of course, I mean that std::is_same<Widget, const Widget>::value would be false. In principle, I agree with you about not using std::decay (or anything else) when you don’t really want the functionality that it provides, but I also tend to prefer to employ standard functionality when I can, because it reduces the cognitive overhead of figuring out what somebody’s custom functionality really does. In this case, a well-named custom metafunction (e.g., remove_cv_and_refs_t) would probably be preferable to using std::decay, but std::decay has the advantage that everybody should either know or be able to trivially look up what it does. In this case, std::decay was a hammer sitting on the shelf in front of me, and what I had was essentially a nail, so…

        BTW, is there a way to be notified when follups to this thread appear? I found out about your followup to my comment purely by happenstance.

        • Scott, I just added a “subscribe to comments” option when submitting comments. If you give it a shot, let me know how it’s working. Thanks for the suggestion.

  4. Pingback: Too perfect forwarding | Andrzej's C++ blog

  5. Looking into my code from a few years back, I see:

    template<class...U,class=typename std::enable_if<!std::is_same<std::tuple<typename std::decay<U>::type...>,std::tuple<wrapper> >::value>::type> wrapper(U&&...u)

    The tuple trick was nice, but I guess I’ll have to drop it and write a proper helper to handle is_base_of (thanks for pointing that out).

  6. I recently was thinking about this problem again, and I think I may have an insight that I would like to share with you. I think the preferred solution is to pass T by value, since it is a sink argument as Sean Parent advocates. But this is perhaps impossible in some cases (e.g. variadic templates, and efficiency). Now, my thought here is that an alternative solution should stay consistent to the pass-by-value behavior in terms of binding. Based on that thought, I don’t think that “U cannot be same or derived from T” is an accurate enough constraint. Suppose there’s another constructor which takes const std::vector<T> & as a parameter, then the perfect-forwarding constructor would bind std::vector<T> & arguments similar to the copy constructor situation. I think it would be more accurate to constrain the U with std::is_convertible<U, T>. This way it accurately mimics the binding behavior of the pass-by-value constructor while still perfect-forwarding. If we consider universal reference as a universal set, I think we want to intersect it with the cases we want it to handle (how pass-by-value behaves), rather than taking away the is-same-or-derived-from-wrapper set. Since taking away the wrapper-specific set from a universal set still leaves too many sets that don’t belong there. Note: I realize this was a post from a long time ago at this point, it’s possible you have updated thoughts about this since then which I would be interested to hear about. I’ve written about this problem as my first blog post: http://mpark.github.io/programming/2014/06/07/beware-of-perfect-forwarding-constructors/

  7. Thank you for this explanation and solution, this saved my day. I was having this problem using a constructor taking a universal reference and copy constructor. I saw that the constructor taking the universal reference was being called where the copy constructor should have been called. I was thinking of creating a static “Make” function instead of the universal reference constructor, but then I saw your post and followed your solution – it’s working great! Thanks again! :)

  8. Eric, what’s the return type of your transform adaptor dereference? In particular, cases where the transform functor is an rvalue projection (std::move being the trivial example), and the base range iterator returns by value, are problematic, because the expression decltype is rvalue reference, but if you make this your return type, you’ll return a reference to a temporary.

    The problem are function that return references to their argument, std::move, bound std::min/max etc. I just introduced a trait to mark such functions, so the transformer can switch to returning by value.

    How do you handle this?

    • Eric, what’s the return type of your transform adaptor dereference?

      Whatever the function return type is.

      The problem are function that return references to their argument, std::move, bound std::min/max etc.

      In the general case, it’s a little strange to be using a function that returns a reference to adapt an iterator whose reference type is a value. I’m guessing that more often than not, it’s a bug. One possibility is to static_assert in that case, and have a way for a user to opt-in; e.g. with a special function wrapper. My feeling — not having thought too deeply — is that it’s overly nannying. Folks need to understand what they’re doing.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>