Overloading on universal references almost always leads to the universal reference overload being called more frequently than expected.
Why universal references?
We want to introduce universal references because we can eliminate some inefficiencies via it. For example,
|
|
|
|
petName
is lvalue, soname
is bound to it, and then copied intonames
. Since an lvalue was passed intologAndAdd
, the final copy operation can not be avoided.- the parameter for the second call is rvalue, where a temporary of type
std::string
is explicitly created and gets bound toname
, and thenname
get copied intonames
. In this call, we might optimize that final copy operation with a move operation since we are dealing with an rvalue - similar procedure as the second call but the
std::string
is implicitly created from string literalCandice
. We might optimize the final copy operation by creating thestd::string
object directly inside thestd::multiset
ifemplace
could use the string literal directly as argument, so there’s not even a move operation.
To achieve such optimization, we find universal reference, accompany with std::forward
:
|
|
What trouble universal references introduces?
Functions taking universal references are the greediest functions in C++, so the usually overload more argument types than the developer generally expects. Suppose there’s another function overloading for the type of int
:
|
|
|
|
We generally assume the last call will invoke the int
overload, but instead it will invoke the T&&
one, since the universal reference overload version exactly matches the short
argument by deduce T
to be short&
, while int
version has to match short
with a promotion. As a result, the exact match beats a match with a promotion. However, within the T&&
overload, the parameter name
with type short&
first get passed into std::forward
, which will not get casted into a rvalue since name
is initialized with an lvalue (refer to EMCpp item 24), and then get passed into emplace
member function on names
, which finally forwards it to the std::string
constructor, and we get an error here because no constructor for std::string
will take a short.
More problematic: Perfect-forwarding consturctors
Perfect-forwarding constructors are typically better matches than copy constructors for non-const
lvalues, and they can hijack derived class calls to base class copy and move constructors, which makes them problematic. For example:
|
|
Remember in EMCpp item 17 we mensioned that, under appropriate conditions, the compiler will generate copy and move constructors for us, even if the class contains a templatized constructor that could be instantiated to produce the signature of the copy or move constructor. In that case, we get following two compiler-generated member functions:
|
|
And now comes the problem: these two member functions will easily get shadowed by the universal reference constructor:
|
|
Moreover, the inheritance makes the mess even worse:
|
|
Here, the derived class will call the perfect forwarding constructor for their copy and move constructors, because they are using arguments of type SpecialPerson
to pass to their base class, and base class’s forwarding constructor will happily instantiate an exact match for this call. Since there’s no std::string
constructor taking a SpecialPerson
, the code won’t compile.
Conclusion
Overloading on universal reference is something we should avoid if possible. However, if we do want a function that forwards most argument types, while still support special treatment for some special types, we can find some alternatives to achieve this goal in EMCpp item 27.