There are two logAndAdd
overloads. The one taking a universal reference can deduce T to be short
, thus yielding an exact match. The overload with an int
parameter can match the short
argument only with a promotion. Per the normal overload resolution rules, an exact match beats a match with a promotion, so the universal reference overload is invoked.
Within that overload, the parameter name
is bound to the short that's passed in. name
is then std::forward
ed to the emplace member function on names
(a std::multiset
), which, in turn, dutifully forwards it to the std::string
constructor. There is no constructor for std::string
that takes a short
, so the std::string
constructor call inside the call to multiset::emplace
inside the call to logAndAdd
fails. All because the universal reference overload was a better match for a short
argument than an int
.
Functions taking universal references are the greediest functions in C++. They instantiate to create exact matches for almost any type of argument. (The few kinds of arguments where this isn't the case are described in Item 30.) This is why combining overloading and universal references is almost always a bad idea: the universal reference overload vacuums up far more argument types than the developer doing the overloading generally expects.
An easy way to topple into this pit is to write a perfect forwarding constructor. A small modification to the logAndAdd
example demonstrates the problem. Instead of writing a free function that can take either a std::string or an index that can be used to look up a std::string
, imagine a class Person
with constructors that do the same thing:
class Person {
public:
template
explicit Person(T&& n) // perfect forwarding ctor;
: name(std::forward(n)) {} // initializes data member
explicit Person(int idx) // int ctor
: name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
As was the case with logAndAdd
, passing an integral type other than int
(e.g., std::size_t
, short
, long
, etc.) will call the universal reference constructor overload instead of the int overload, and that will lead to compilation failures. The problem here is much worse, however, because there's more overloading present in Person
than meets the eye. Item 17explains that under the appropriate conditions, C++ will generate both copy and move constructors, and this is true even if the class contains a templatized constructor that could be instantiated to produce the signature of the copy or move constructor. If the copy and move constructors for Person
are thus generated, Person
will effectively look like this:
class Person {
public:
template // perfect forwarding ctor
explicit Person(T&& n)
: name(std::forward(n)) {}
explicit Person(int idx); // int ctor
Person(const Person& rhs);// copy ctor
// (compiler-generated)
Person(Person&& rhs); // move ctor
… // (compiler-generated)
};
This leads to behavior that's intuitive only if you've spent so much time around compilers and compiler-writers, you've forgotten what it's like to be human:
Person p("Nancy");
auto cloneOfP(p); // create new Person from p;
// this won't compile!
Here we're trying to create a Person
from another Person
, which seems like about as obvious a case for copy construction as one can get. ( p
's an lvalue, so we can banish any thoughts we might have about the “copying” being accomplished through a move operation.) But this code won't call the copy constructor. It will call the perfect- forwarding constructor. That function will then try to initialize Person
's std::string
data member with a Person
object ( p
). std::string
having no constructor taking a Person
, your compilers will throw up their hands in exasperation, possibly punishing you with long and incomprehensible error messages as an expression of their displeasure.
“Why,” you might wonder, “does the perfect-forwarding constructor get called instead of the copy constructor? We're initializing a Person
with another Person
!” Indeed we are, but compilers are sworn to uphold the rules of C++, and the rules of relevance here are the ones governing the resolution of calls to overloaded functions.
Compilers reason as follows. cloneOfP
is being initialized with a non -const
lvalue ( p
), and that means that the templatized constructor can be instantiated to take a non- const
lvalue of type Person
. After such instantiation, the Person
class looks like this:
class Person {
public:
explicit Person(Person& n) // instantiated from
: name(std::forward(n)) {} // perfect-forwarding
// template
explicit Person(int idx); // as before
Person(const Person& rhs); // copy ctor
… // (compiler-generated)
};
In the statement,
auto cloneOfP(p);
p
could be passed to either the copy constructor or the instantiated template. Calling the copy constructor would require adding const
to p
to match the copy constructor's parameter's type, but calling the instantiated template requires no such addition. The overload generated from the template is thus a better match, so compilers do what they're designed to do: generate a call to the better-matching function. “Copying” non- const
lvalues of type Person
is thus handled by the perfect-forwarding constructor, not the copy constructor.
If we change the example slightly so that the object to be copied is const
, we hear an entirely different tune:
constPerson cp("Nancy"); // object is now const
auto cloneOfP(cp); // calls copy constructor!
Because the object to be copied is now const
, it's an exact match for the parameter taken by the copy constructor. The templatized constructor can be instantiated to have the same signature,
class Person {
public:
explicit Person(const Person& n);// instantiated from
// template
Person(const Person& rhs); // copy ctor
// (compiler-generated)
…
};
but this doesn't matter, because one of the overload-resolution rules in C++ is that in situations where a template instantiation and a non-template function (i.e., a “normal” function) are equally good matches for a function call, the normal function is preferred. The copy constructor (a normal function) thereby trumps an instantiated template with the same signature.
(If you're wondering why compilers generate a copy constructor when they could instantiate a templatized constructor to get the signature that the copy constructor would have, review Item 17.)
Читать дальше