Universal reference parameters often have efficiency advantages, but they typically have usability disadvantages.
Abandon overloading
This solution works for overloaded logAndAdd
example in Item 26, where we break the overloaded function into two: logAndAddName
and logAndAddIdx
. However, this will not work for Person
constructor - the constructor names are fixed by the language.
Pass by const T&
This is the original function void logAndAdd(const std::string& name)
we see in Item 26. Not efficient in some cases, but works as expected.
Pass by value
According to the advice in Item 41, we may consider passing objects by value when we know we’ll copy them. Thus, the Person
example may get revised like this:
|
|
With this design, int
-like arguments get passed to int
overload, and arguments of type std::string
(and anything from which std::string
could be created, e.g., literals) get passed to the std::string
overload.
Use Tag dispatch
Add another “tag” parameter to help compiler differentiate the overloading cases as we want:
|
|
Conceptually, true
and false
are runtime values, and what we need here for the tag parameter should be compil-time types that corresponds to true
and false
, which in the Standard Library are called std::true_type
and std::false_type
. This compile-time variables serve no purpose at runtime, so some compilers who’s smart enough may recognize these tag parameters and optimize them out of the program’s execution image.
Tag dispatch is a standard building block of template metaprogramming to let the tag determine which overload gets called, so that overloading on universal references may work as expect.
Use enable_if
to constrain templates that take universal references
Tag dispatch solves some of the problems related with templates taking universal references, but not all of them. The perfect-forwarding constructor for the Person
class, for example, remains problematic: even if we write only one constructor and apply tag dispactch technique to it, some constructor calls (copy from const
vs non-const
lvalues) may sometimes be handled by compiler-generated functions (e.g., copy and move constructors) that bypass the tag dispatch system.
Thus, we want to constrain on when the function template is permitted to be employed. By default, all templates are enabled, but a template using std::enable_if
is enabled only if the condition specified by std::enable_if
is satisfied.
In the case of Person
’s perfect forwarding constructor, we want to enable its instantiation only if the type being passed isn’t Person
, so that the class’s copy or move constructor my handle the calls where a Person
object gets passed in. Specifically, when checking the type of the argument being passed, we want to ignore its referenceness, constness, and volatileness using std::decay<T>
:
|
|
Moreover, if we want to make sure the derived class work properly, the conditions for std::enble_if
get more restricted: we want to enable it for any argument type other than Person
or a type derived from Person
. To determian whether one type is derived from another, we can use std::is_base_of<T1, T2>
:
|
|
In C++14, by employing alias templates, we can save some typing for typename
and ::type
:
|
|
Finally, to get our perfect forwarding constructor to work with the int
overload, we have to add another constrain in std::enbale_if
to check the integral arguments type:
|
|
To make even more effective code, considering some kinds of arguments can’t be perfect-forwarded, as well as the fact that forwarding functions tend to create lengthy error messages, which is debug-unfriendly, we can use static_assert
, accompanied with std::is_constructible
, to perform a compile-time test to determine whether an object of one type can be constructed from an object (or a set of objects) of a different type (or set of types):
|
|