But perfect forwarding has drawbacks. One is that some kinds of arguments can't be perfect-forwarded, even though they can be passed to functions taking specific types. Item 30explores these perfect forwarding failure cases.
A second issue is the comprehensibility of error messages when clients pass invalid arguments. Suppose, for example, a client creating a Person
object passes a string literal made up of char16_t
s (a type introduced in C++11 to represent 16-bit characters) instead of char
s (which is what a std::string
consists of):
Person p( u"Konrad Zuse"); // "Konrad Zuse" consists of
// characters of type const char16_t
With the first three approaches examined in this Item, compilers will see that the available constructors take either int
or std::string
, and they'll produce a more or less straightforward error message explaining that there's no conversion from const char16_t[12]
to int
or std::string
.
With an approach based on perfect forwarding, however, the array of const char16_t
s gets bound to the constructor's parameter without complaint. From there it's forwarded to the constructor of Person
's std::string
data member, and it's only at that point that the mismatch between what the caller passed in (a const char16_t
array) and what's required (any type acceptable to the std::string
constructor) is discovered. The resulting error message is likely to be, er, impressive. With one of the compilers I use, it's more than 160 lines long.
In this example, the universal reference is forwarded only once (from the Person
constructor to the std::string
constructor), but the more complex the system, the more likely that a universal reference is forwarded through several layers of function calls before finally arriving at a site that determines whether the argument type(s) are acceptable. The more times the universal reference is forwarded, the more baffling the error message may be when something goes wrong. Many developers find that this issue alone is grounds to reserve universal reference parameters for interfaces where performance is a foremost concern.
In the case of Person
, we know that the forwarding function's universal reference parameter is supposed to be an initializer for a std::string
, so we can use a static_assert
to verify that it can play that role. The std::is_constructible
type trait performs a compile-time test to determine whether an object of one type can be constructed from an object (or set of objects) of a different type (or set of types), so the assertion is easy to write:
class Person {
public:
template< // as before
typename T,
typename = std::enable_if_t<
!std::is_base_of>::value
&&
!std::is_integral>::value
>
>
explicit Person(T&& n)
: name(std::forward(n)) {
// assert that a std::string can be created from a T object
static_assert(
std::is_constructible::value,
"Parameter n can't be used to construct a std::string"
);
… // the usual ctor work goes here
}
… // remainder of Person class (as before)
};
This causes the specified error message to be produced if client code tries to create a Person
from a type that can't be used to construct a std::string
. Unfortunately, in this example the static_assert
is in the body of the constructor, but the forwarding code, being part of the member initialization list, precedes it. With the compilers I use, the result is that the nice, readable message arising from the static_assert
appears only after the usual error messages (up to 160-plus lines of them) have been emitted.
Things to Remember
• Alternatives to the combination of universal references and overloading include the use of distinct function names, passing parameters by lvalue reference-to- const
, passing parameters by value, and using tag dispatch.
• Constraining templates via std::enable_if
permits the use of universal references and overloading together, but it controls the conditions under which compilers may use the universal reference overloads.
• Universal reference parameters often have efficiency advantages, but they typically have usability disadvantages.
Item 28: Understand reference collapsing.
Item 23remarks that when an argument is passed to a template function, the type deduced for the template parameter encodes whether the argument is an lvalue or an rvalue. The Item fails to mention that this happens only when the argument is used to initialize a parameter that's a universal reference, but there's a good reason for the omission: universal references aren't introduced until Item 24. Together, these observations about universal references and lvalue/rvalue encoding mean that for this template,
template
void func(T&& param);
the deduced template parameter T
will encode whether the argument passed to param
was an lvalue or an rvalue.
The encoding mechanism is simple. When an lvalue is passed as an argument, T
is deduced to be an lvalue reference. When an rvalue is passed, T
is deduced to be a non-reference. (Note the asymmetry: lvalues are encoded as lvalue references, but rvalues are encoded as non-references .) Hence:
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced
// to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced
// to be Widget
In both calls to func
, a Widget
is passed, yet because one Widget
is an lvalue and one is an rvalue, different types are deduced for the template parameter T
. This, as we shall soon see, is what determines whether universal references become rvalue references or lvalue references, and it's also the underlying mechanism through which std::forward
does its work.
Before we can look more closely at std::forward
and universal references, we must note that references to references are illegal in C++. Should you try to declare one, your compilers will reprimand you:
int x;
…
auto & &rx = x; // error! can't declare reference to reference
But consider what happens when an lvalue is passed to a function template taking a universal reference:
template
void func(T&& param); // as before
func(w); // invoke func with lvalue;
// T deduced as Widget&
If we take the type deduced for T
(i.e., Widget&
) and use it to instantiate the template, we get this:
void func( Widget& &¶m);
A reference to a reference! And yet compilers issue no protest. We know from Item 24that because the universal reference param
is being initialized with an lvalue, param
's type is supposed to be an lvalue reference, but how does the compiler get from the result of taking the deduced type for T and substituting it into the template to the following, which is the ultimate function signature?
Читать дальше