names.emplace(std::forward(name));
}
By itself, this function works fine, but were we to introduce the overload taking an int
that's used to look up objects by index, we'd be back in the troubled land of Item 26. The goal of this Item is to avoid that. Rather than adding the overload, we'll reimplement logAndAdd
to delegate to two other functions, one for integral values and one for everything else. logAndAdd
itself will accept all argument types, both integral and non-integral.
The two functions doing the real work will be named logAndAddImpl
, i.e., we'll use overloading. One of the functions will take a universal reference. So we'll have both overloading and universal references. But each function will also take a second parameter, one that indicates whether the argument being passed is integral. This second parameter is what will prevent us from tumbling into the morass described in Item 26, because we'll arrange it so that the second parameter will be the factor that determines which overload is selected.
Yes, I know, “Blah, blah, blah. Stop talking and show me the code!” No problem. Here's an almost-correct version of the updated logAndAdd
:
template
void logAndAdd(T&& name) {
logAndAddImpl(std::forward(name),
std::is_integral()); // not quite correct
}
This function forwards its parameter to logAndAddImpl
, but it also passes an argument indicating whether that parameter's type ( T
) is integral. At least, that's what it's supposed to do. For integral arguments that are rvalues, it's also what it does. But, as Item 28explains, if an lvalue argument is passed to the universal reference name
, the type deduced for T will be an lvalue reference. So if an lvalue of type int
is passed to logAndAdd
, T
will be deduced to be int&
. That's not an integral type, because references aren't integral types. That means that std::is_integral
will be false for any lvalue argument, even if the argument really does represent an integral value.
Recognizing the problem is tantamount to solving it, because the ever-handy Standard C++ Library has a type trait (see Item 9), std::remove_reference
, that does both what its name suggests and what we need: remove any reference qualifiers from a type. The proper way to write logAndAdd is therefore:
template
void logAndAdd(T&& name) {
logAndAddImpl(
std::forward(name),
std::is_integral< typename std::remove_reference::type>()
);
}
This does the trick. (In C++14, you can save a few keystrokes by using std::remove_reference_t
in place of the highlighted text. For details, see Item 9.)
With that taken care of, we can shift our attention to the function being called, logAndAddImpl
. There are two overloads, and the first is applicable only to non-integral types (i.e., to types where std::is_integral::type>
is false):
template // non-integral
void logAndAddImpl(T&& name, std::false_type) // argument:
{ // add it to
auto now = std::chrono::system_clock::now(); // global data
log(now, "logAndAdd"); // structure
names.emplace(std::forward(name));
}
This is straightforward code, once you understand the mechanics behind the highlighted parameter. Conceptually, logAndAdd
passes a boolean to logAndAddImpl
indicating whether an integral type was passed to logAndAdd
, but true
and false
are runtime values, and we need to use overload resolution — a compile-time phenomenon — to choose the correct logAndAddImpl
overload. That means we need a type that corresponds to true
and a different type that corresponds to false
. This need is common enough that the Standard Library provides what is required under the names std::true_type
and std::false_type
. The argument passed to logAndAddImpl
by logAndAdd
is an object of a type that inherits from std::true_type
if T
is integral and from std::false_type
if T
is not integral. The net result is that this logAndAddImpl
overload is a viable candidate for the call in logAndAdd
only if T
is not an integral type.
The second overload covers the opposite case: when T
is an integral type. In that event, logAndAddImpl
simply finds the name corresponding to the passed-in index and passes that name back to logAndAdd
:
std::string nameFromIdx(int idx); // as in Item 26
void logAndAddImpl(int idx, std::true_type) // integral
{ // argument: look
logAndAdd(nameFromIdx(idx)); // up name and
} // call logAndAdd
// with it
By having logAndAddImpl
for an index look up the corresponding name and pass it to logAndAdd
(from where it will be std::forward
ed to the other logAndAddImpl
overload), we avoid the need to put the logging code in both logAndAddImpl
overloads.
In this design, the types std::true_type
and std::false_type
are “tags” whose only purpose is to force overload resolution to go the way we want. Notice that we don't even name those parameters. They serve no purpose at runtime, and in fact we hope that compilers will recognize that the tag parameters are unused and will optimize them out of the program's execution image. (Some compilers do, at least some of the time.) The call to the overloaded implementation functions inside logAndAdd
“dispatches” the work to the correct overload by causing the proper tag object to be created. Hence the name for this design: tag dispatch . It's a standard building block of template metaprogramming, and the more you look at code inside contemporary C++ libraries, the more often you'll encounter it.
For our purposes, what's important about tag dispatch is less how it works and more how it permits us to combine universal references and overloading without the problems described in Item 26. The dispatching function — logAndAdd
— takes an unconstrained universal reference parameter, but this function is not overloaded. The implementation functions — logAndAddImpl
— are overloaded, and one takes a universal reference parameter, but resolution of calls to these functions depends not just on the universal reference parameter, but also on the tag parameter, and the tag values are designed so that no more than one overload will be a viable match. As a result, it's the tag that determines which overload gets called. The fact that the universal reference parameter will always generate an exact match for its argument is immaterial.
Constraining templates that take universal references
A keystone of tag dispatch is the existence of a single (unoverloaded) function as the client API. This single function dispatches the work to be done to the implementation functions. Creating an unoverloaded dispatch function is usually easy, but the second problem case Item 26considers, that of a perfect-forwarding constructor for the Person
class (shown on page 178), is an exception. Compilers may generate copy and move constructors themselves, so even if you write only one constructor and use tag dispatch within it, some constructor calls may be handled by compiler-generated functions that bypass the tag dispatch system.
Читать дальше