Success, right? We're done!
Um, no. Belay that celebration. There's still one loose end from Item 26that continues to flap about. We need to tie it down.
Suppose a class derived from Person
implements the copy and move operations in the conventional manner:
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!
…
};
This is the same code I showed in Item 26(on page 206), including the comments, which, alas, remain accurate. When we copy or move a SpecialPerson
object, we expect to copy or move its base class parts using the base class's copy and move constructors, but in these functions, we're passing SpecialPerson
objects to the base class's constructors, and because SpecialPerson
isn't the same as Person
(not even after application of std::decay
), the universal reference constructor in the base class is enabled, and it happily instantiates to perform an exact match for a SpecialPerson
argument. This exact match is better than the derived-to-base conversions that would be necessary to bind the SpecialPerson
objects to the Person
parameters in Person
's copy and move constructors, so with the code we have now, copying and moving SpecialPerson
objects would use the Person
perfect-forwarding constructor to copy or move their base class parts! It's déjà Item 26all over again.
The derived class is just following the normal rules for implementing derived class copy and move constructors, so the fix for this problem is in the base class and, in particular, in the condition that controls whether Person
's universal reference constructor is enabled. We now realize that we don't want to enable the templatized constructor for any argument type other than Person
, we want to enable it for any argument type other than Person
or a type derived from Person
. Pesky inheritance!
You should not be surprised to hear that among the standard type traits is one that determines whether one type is derived from another. It's called std::is_base_of
. std::is_base_of::value
is true if T2
is derived from T1
. Types are considered to be derived from themselves, so std::is_base_of::value
is true. This is handy, because we want to revise our condition controlling Person
's perfect- forwarding constructor such that the constructor is enabled only if the type T
, after stripping it of references and cv-qualifiers, is neither Person
nor a class derived from Person
. Using std::is_base_of
instead of std::is_same
gives us what we need:
class Person {
public:
template<
typename T,
typename = typename std::enable_if<
!std:: is_base_of
typename std::decay::type
>::value
>::type
>
explicit Person(T&& n);
…
};
Now we're finally done. Provided we're writing the code in C++11, that is. If we're using C++14, this code will still work, but we can employ alias templates for std::enable_if
and std::decay
to get rid of the “ typename
” and “ ::type
” cruft, thus yielding this somewhat more palatable code:
class Person { // C++14
public:
template<
typename T,
typename = std::enable_if _t< // less code here
!std::is_base_of
std::decay _t // and here
>::value
> // and here
>
explicit Person(T&& n);
…
};
Okay, I admit it: I lied. We're still not done. But we're close. Tantalizingly close. Honest.
We've seen how to use std::enable_if
to selectively disable Person
's universal reference constructor for argument types we want to have handled by the class's copy and move constructors, but we haven't yet seen how to apply it to distinguish integral and non-integral arguments. That was, after all, our original goal; the constructor ambiguity problem was just something we got dragged into along the way.
All we need to do — and I really do mean that this is everything — is (1) add a Person
constructor overload to handle integral arguments and (2) further constrain the templatized constructor so that it's disabled for such arguments. Pour these ingredients into the pot with everything else we've discussed, simmer over a low flame, and savor the aroma of success:
class Person {
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of>::value
&&
!std::is_integral>::value
>
>
explicit Person(T&& n) // ctor for std::strings and
: name(std::forward(n)) // args convertible to
{ … } // std::strings
explicit Person(int idx) // ctor for integral args
: name(nameFromIdx(idx))
{ … }
… // copy and move ctors, etc.
private:
std::string name;
};
Voilà! A thing of beauty! Well, okay, the beauty is perhaps most pronounced for those with something of a template metaprogramming fetish, but the fact remains that this approach not only gets the job done, it does it with unique aplomb. Because it uses perfect forwarding, it offers maximal efficiency, and because it controls the combination of universal references and overloading rather than forbidding it, this technique can be applied in circumstances (such as constructors) where overloading is unavoidable.
Trade-offs
The first three techniques considered in this Item — abandoning overloading, passing by const T&
, and passing by value — specify a type for each parameter in the function(s) to be called. The last two techniques — tag dispatch and constraining template eligibility — use perfect forwarding, hence don't specify types for the parameters. This fundamental decision — to specify a type or not — has consequences.
As a rule, perfect forwarding is more efficient, because it avoids the creation of temporary objects solely for the purpose of conforming to the type of a parameter declaration. In the case of the Person
constructor, perfect forwarding permits a string literal such as "Nancy"
to be forwarded to the constructor for the std::string
inside Person
, whereas techniques not using perfect forwarding must create a temporary std::string
object from the string literal to satisfy the parameter specification for the Person
constructor.
Читать дальше