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 Personimplements 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 SpecialPersonobject, 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 SpecialPersonobjects to the base class's constructors, and because SpecialPersonisn'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 SpecialPersonargument. This exact match is better than the derived-to-base conversions that would be necessary to bind the SpecialPersonobjects to the Personparameters in Person's copy and move constructors, so with the code we have now, copying and moving SpecialPersonobjects would use the Personperfect-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::valueis true if T2is derived from T1. Types are considered to be derived from themselves, so std::is_base_of::valueis 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 Personnor a class derived from Person. Using std::is_base_ofinstead of std::is_samegives 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_ifand std::decayto 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_ifto 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 Personconstructor 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 Personconstructor, perfect forwarding permits a string literal such as "Nancy"to be forwarded to the constructor for the std::stringinside Person, whereas techniques not using perfect forwarding must create a temporary std::stringobject from the string literal to satisfy the parameter specification for the Personconstructor.
Читать дальше