The interaction among perfect-forwarding constructors and compiler-generated copy and move operations develops even more wrinkles when inheritance enters the picture. In particular, the conventional implementations of derived class copy and move operations behave quite surprisingly. Here, take a look:
class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs) // copy ctor; calls
: Person(rhs) // base class
{ … } // forwarding ctor!
SpecialPerson(SpecialPerson&& rhs) // move ctor; calls
: Person(std::move(rhs)) // base class
{ … } // forwarding ctor!
};
As the comments indicate, the derived class copy and move constructors don't call their base class's copy and move constructors, they call the base class's perfect- forwarding constructor! To understand why, note that the derived class functions are using arguments of type SpecialPerson
to pass to their base class, then work through the template instantiation and overload-resolution consequences for the constructors in class Person
. Ultimately, the code won't compile, because there's no std::string
constructor taking a SpecialPerson
.
I hope that by now I've convinced you that overloading on universal reference parameters is something you should avoid if at all possible. But if overloading on universal references is a bad idea, what do you do if you need a function that forwards most argument types, yet needs to treat some argument types in a special fashion? That egg can be unscrambled in a number of ways. So many, in fact, that I've devoted an entire Item to them. It's Item 27. The next Item. Keep reading, you'll bump right into it.
Things to Remember
• Overloading on universal references almost always leads to the universal reference overload being called more frequently than expected.
• Perfect-forwarding constructors are especially problematic, because they're typically better matches than copy constructors for non- const
lvalues, and they can hijack derived class calls to base class copy and move constructors.
Item 27: Familiarize yourself with alternatives to overloading on universal references.
Item 26explains that overloading on universal references can lead to a variety of problems, both for freestanding and for member functions (especially constructors). Yet it also gives examples where such overloading could be useful. If only it would behave the way we'd like! This Item explores ways to achieve the desired behavior, either through designs that avoid overloading on universal references or by employing them in ways that constrain the types of arguments they can match.
The discussion that follows builds on the examples introduced in Item 26. If you haven't read that Item recently, you'll want to review it before continuing.
Abandon overloading
The first example in Item 26, logAndAdd
, is representative of the many functions that can avoid the drawbacks of overloading on universal references by simply using different names for the would-be overloads. The two logAndAdd
overloads, for example, could be broken into logAndAddName
and logAndAddNameIdx
. Alas, this approach won't work for the second example we considered, the Person constructor, because constructor names are fixed by the language. Besides, who wants to give up overloading?
Pass by const T&
An alternative is to revert to C++98 and replace pass-by-universal-reference with pass-by-lvalue-reference-to- const
. In fact, that's the first approach Item 26considers (shown on page 175). The drawback is that the design isn't as efficient as we'd prefer. Knowing what we now know about the interaction of universal references and overloading, giving up some efficiency to keep things simple might be a more attractive trade-off than it initially appeared.
Pass by value
An approach that often allows you to dial up performance without any increase in complexity is to replace pass-by-reference parameters with, counterintuitively, pass by value. The design adheres to the advice in Item 41to consider passing objects by value when you know you'll copy them, so I'll defer to that Item for a detailed discussion of how things work and how efficient they are. Here, I'll just show how the technique could be used in the Person
example:
class Person {
public:
explicit Person( std::stringn) // replaces T&& ctor; see
: name(std::move(n)) {} // Item 41 for use of std::move
explicit Person(int idx) // as before
: name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
Because there's no std::string
constructor taking only an integer, all int
and int
-like arguments to a Person
constructor (e.g., std::size_t
, short
, long
) get funnelled to the int
overload. Similarly, all arguments of type std::string
(and things from which std::string
s can be created, e.g., literals such as "Ruth"
) get passed to the constructor taking a std::string
. There are thus no surprises for callers. You could argue, I suppose, that some people might be surprised that using 0
or NULL
to indicate a null pointer would invoke the int overload, but such people should be referred to Item 8and required to read it repeatedly until the thought of using 0
or NULL
as a null pointer makes them recoil.
Use Tag dispatch
Neither pass by lvalue-reference-to- const
nor pass by value offers support for perfect forwarding. If the motivation for the use of a universal reference is perfect forwarding, we have to use a universal reference; there's no other choice. Yet we don't want to abandon overloading. So if we don't give up overloading and we don't give up universal references, how can we avoid overloading on universal references?
It's actually not that hard. Calls to overloaded functions are resolved by looking at all the parameters of all the overloads as well as all the arguments at the call site, then choosing the function with the best overall match — taking into account all parameter/argument combinations. A universal reference parameter generally provides an exact match for whatever's passed in, but if the universal reference is part of a parameter list containing other parameters that are not universal references, sufficiently poor matches on the non-universal reference parameters can knock an overload with a universal reference out of the running. That's the basis behind the tag dispatch approach, and an example will make the foregoing description easier to understand.
We'll apply tag dispatch to the logAndAdd
example on page 177. Here's the code for that example, lest you get sidetracked looking it up:
std::multiset names; // global data structure
template // make log entry and add
void logAndAdd(T&& name) // name to data structure
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
Читать дальше