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 -const
lvalues. 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_if
gives 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_if
is enabled only if the condition specified by std::enable_if
is satisfied. In our case, we'd like to enable the Person
perfect-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 Person
object 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_if
as is required simply to use it. I'm showing only the declaration for this constructor, because the use of std::enable_if
has 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_if
work.) 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 T
isn'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 Person
and T
to 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 T
in the universal constructor will be deduced to be Person&
. The types Person
and Person&
are not the same, and the result of std::is_same
will reflect that: std::is_same::value
is false.
If we think more precisely about what we mean when we say that the templatized constructor in Person
should be enabled only if T
isn'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 Person
and a volatile Person
and a const volatile Person
are all the same as a Person
.
This means we need a way to strip any references, const
s, and volatile
s from T
before 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::type
is the same as T
, except that references and cv-qualifiers (i.e., const
or volatile
qualifiers) 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::decay
behaves as I've described.) The condition we want to control whether our constructor is enabled, then, is
!std::is_same::type>::value
i.e., Person
is not the same type as T
, ignoring any references or cv-qualifiers. (As Item 9explains, the “ typename
” in front of std::decay
is required, because the type std::decay::type
depends on the template parameter T
.)
Inserting this condition into the std::enable_if
boilerplate 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 Person
from another Person
— lvalue or rvalue, const
or non- const
, volatile
or non- volatile
— will never invoke the constructor taking a universal reference.
Читать дальше