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!
Shouldn’t it be:
typename X = typename
disable_if_is_base_of<wrapper<T>,U>
::type
?
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
andwrapper<T>
are synonyms.I believe that’s called the injected-class-name
You forgot a
::type
part afterdisable_if_is_base_of<wrapper,U>
and atypename
before.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
andtypename
as you suggest. But based on feedback, and on reflection, I decided a template alias would be better, and I renamed it todisable_if_same_or_derived
. In this formulation, no::type
ortypename
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.
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 thefoo
member via its copy constructor. Anything else would be surprising.Sadly the
delete =
stuff on a copy ctor also doesn’t work – i.e., it doesn’t show any meaningful message, only the “failed to instantiate” bs.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!
}
@Bartosz Bielecki: I don’t think you want to use
std::is_same
, becausestd::is_same
takes cv-qualifiers into account. That means thatstd::is_same<wrapper, const wrapper>::value
is 1, which is not what you want. You can usestd::is_base_of
, as Eric did, because that ignores cv-qualifiers. (It also handles inheritance, as Eric pointed out.) Or you can usestd::decay
instead ofstd::remove_reference
onU
, becausestd::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 usingstd::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 ownremove_cvref
trait and use that instead.@Eric: Yes, of course, I mean that
std::is_same<Widget, const Widget>::value
would befalse
. In principle, I agree with you about not usingstd::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 usingstd::decay,
butstd::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.
Pingback: Too perfect forwarding | Andrzej's C++ blog
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).
What’s the solution/workaround for the variadic template case?
I ran into the issue of forwarding variadic templates a while ago. Have a look at this stackoverflow post:
http://stackoverflow.com/q/13296461/1170277
Does not marking forwarding ctor as explicit help to exclude it from overloading with real copy ctor?
Sadly not.
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 fromT
” is an accurate enough constraint. Suppose there’s another constructor which takesconst std::vector<T> &
as a parameter, then the perfect-forwarding constructor would bindstd::vector<T> &
arguments similar to the copy constructor situation. I think it would be more accurate to constrain theU
withstd::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 theis-same-or-derived-from-wrapper
set. Since taking away thewrapper
-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/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! 🙂
Glad it was useful!
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?
Whatever the function return type is.
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.Pingback: Does “Templatisation” Always Result in Equivalent Code? | Programming
Sorry for beating a dead horse, but the problem can be solved by simply adding non-const copy ctor:
template<typename T>
struct wrapper
{
T value;
wrapper(wrapper&) = default;
template<typename U>
wrapper(U&& u)
: value(std::forward<U>(u))
{}
};
int main()
{
wrapper<std::string> str { "foo" };
wrapper<std::string> str2 = str;
}
Sorry about the formatting. No idea how to make code blocks in WP.
You’re missing the point.. you’d need a constructor for every other Cartesian combination of
{const,volatile, both, none} x {&,&&}
, or the universal version will be a better match. And the real problem isn’t even this: Construction from any derived class (and the const etc. combination thereof) would also match the universal version.dear author can you please let me know how universal references are different from constant references to copy constructor. I read about copy constructor from here https://www.tutorialcup.com/cplusplus/copy-constructor.htm and after reading your article I came to know about new work “universal reference”