In truth, the real problem is not that the compiler-generated functions sometimes bypass the tag dispatch design, it's that they don't always pass it by. You virtually always want the copy constructor for a class to handle requests to copy lvalues of that type, but, as Item 26demonstrates, providing a constructor taking a universal reference causes the universal reference constructor (rather than the copy constructor) to be called when copying non -constlvalues. That Item also explains that when a base class declares a perfect-forwarding constructor, that constructor will typically be called when derived classes implement their copy and move constructors in the conventional fashion, even though the correct behavior is for the base class's copy and move constructors to be invoked.
For situations like these, where an overloaded function taking a universal reference is greedier than you want, yet not greedy enough to act as a single dispatch function, tag dispatch is not the droid you're looking for. You need a different technique, one that lets you rachet down the conditions under which the function template that the universal reference is part of is permitted to be employed. What you need, my friend, is std::enable_if.
std::enable_ifgives you a way to force compilers to behave as if a particular template didn't exist. Such templates are said to be disabled . By default, all templates are enabled , but a template using std::enable_ifis enabled only if the condition specified by std::enable_ifis satisfied. In our case, we'd like to enable the Personperfect-forwarding constructor only if the type being passed isn't Person. If the type being passed is Person, we want to disable the perfect-forwarding constructor (i.e., cause compilers to ignore it), because that will cause the class's copy or move constructor to handle the call, which is what we want when a Personobject is initialized with another Person.
The way to express that idea isn't particularly difficult, but the syntax is off-putting, especially if you've never seen it before, so I'll ease you into it. There's some boilerplate that goes around the condition part of std::enable_if, so we'll start with that. Here's the declaration for the perfect-forwarding constructor in Person, showing only as much of the std::enable_ifas is required simply to use it. I'm showing only the declaration for this constructor, because the use of std::enable_ifhas no effect on the function's implementation. The implementation remains the same as in Item 26.
class Person {
public:
template
typename = typename std::enable_if< condition >::type>
explicit Person(T&& n);
…
};
To understand exactly what's going on in the highlighted text, I must regretfully suggest that you consult other sources, because the details take a while to explain, and there's just not enough space for it in this book. (During your research, look into “SFINAE” as well as std::enable_if, because SFINAE is the technology that makes std::enable_ifwork.) Here, I want to focus on expression of the condition that will control whether this constructor is enabled.
The condition we want to specify is that Tisn't Person, i.e., that the templatized constructor should be enabled only if T is a type other than Person. Thanks to a type trait that determines whether two types are the same ( std::is_same), it would seem that the condition we want is !std::is_same::value. (Notice the “ !” at the beginning of the expression. We want for Personand Tto not be the same.) This is close to what we need, but it's not quite correct, because, as Item 28explains, the type deduced for a universal reference initialized with an lvalue is always an lvalue reference. That means that for code like this,
Person p("Nancy");
auto cloneOfP(p); // initialize from lvalue
the type Tin the universal constructor will be deduced to be Person&. The types Personand Person&are not the same, and the result of std::is_samewill reflect that: std::is_same::valueis false.
If we think more precisely about what we mean when we say that the templatized constructor in Personshould be enabled only if Tisn't Person, we'll realize that when we're looking at T, we want to ignore
• Whether it's a reference. For the purpose of determining whether the universal reference constructor should be enabled, the types Person, Person&, and Person&&are all the same as Person.
• Whether it's const or volatile . As far as we're concerned, a const Personand a volatile Personand a const volatile Personare all the same as a Person.
This means we need a way to strip any references, consts, and volatiles from Tbefore checking to see if that type is the same as Person. Once again, the Standard Library gives us what we need in the form of a type trait. That trait is std::decay. std::decay::typeis the same as T, except that references and cv-qualifiers (i.e., constor volatilequalifiers) are removed. (I'm fudging the truth here, because std::decay, as its name suggests, also turns array and function types into pointers (see Item 1), but for purposes of this discussion, std::decaybehaves as I've described.) The condition we want to control whether our constructor is enabled, then, is
!std::is_same::type>::value
i.e., Personis not the same type as T, ignoring any references or cv-qualifiers. (As Item 9explains, the “ typename” in front of std::decayis required, because the type std::decay::typedepends on the template parameter T.)
Inserting this condition into the std::enable_ifboilerplate above, plus formatting the result to make it easier to see how the pieces fit together, yields this declaration for Person's perfect-forwarding constructor:
class Person {
public:
template<
typename T,
typename = typename std::enable_if<
!std::is_same
typename std::decay::type
>::value
>::type
>
explicit Person(T&& n);
…
};
If you've never seen anything like this before, count your blessings. There's a reason I saved this design for last. When you can use one of the other mechanisms to avoid mixing universal references and overloading (and you almost always can), you should. Still, once you get used to the functional syntax and the proliferation of angle brackets, it's not that bad. Furthermore, this gives you the behavior you've been striving for. Given the declaration above, constructing a Personfrom another Person— lvalue or rvalue, constor non- const, volatileor non- volatile— will never invoke the constructor taking a universal reference.
Читать дальше