We’ve seen that, by default, the compiler uses the arguments in a call to determine the template parameters for a function template. The process of determining the template arguments from the function arguments is known as template argument deduction. During template argument deduction, the compiler uses types of the arguments in the call to find the template arguments that generate a version of the function that best matches the given call.
As with a nontemplate function, the arguments we pass in a call to a function template are used to initialize that function’s parameters. Function parameters whose type uses a template type parameter have special initialization rules. Only a very limited number of conversions are automatically applied to such arguments. Rather than converting the arguments, the compiler generates a new instantiation.
As usual, top-level const
s (§ 2.4.3, p. 63) in either the parameter or the argument are ignored. The only other conversions performed in a call to a function template are
•
const
conversions: A function parameter that is a reference (or pointer) to aconst
can be passed a reference (or pointer) to a nonconst
object (§ 4.11.2, p. 162).
• Array- or function-to-pointer conversions: If the function parameter is not a reference type, then the normal pointer conversion will be applied to arguments of array or function type. An array argument will be converted to a pointer to its first element. Similarly, a function argument will be converted to a pointer to the function’s type (§ 4.11.2, p. 161).
Other conversions, such as the arithmetic conversions (§ 4.11.1, p. 159), derived-to-base (§ 15.2.2, p. 597), and user-defined conversions (§ 7.5.4, p. 294, and § 14.9, p. 579), are not performed.
As examples, consider calls to the functions fobj
and fref
. The fobj
function copies its parameters, whereas fref
’s parameters are references:
template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored
fref(s1, s2); // calls fref(const string&, const string&)
// uses premissible conversion to const on s1
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: array types don't match
In the first pair of calls, we pass a string
and a const string
. Even though these types do not match exactly, both calls are legal. In the call to fobj
, the arguments are copied, so whether the original object is const
doesn’t matter. In the call to fref
, the parameter type is a reference to const
. Conversion to const
for a reference parameter is a permitted conversion, so this call is legal.
In the next pair of calls, we pass array arguments in which the arrays are different sizes and hence have different types. In the call to fobj
, the fact that the array types differ doesn’t matter. Both arrays are converted to pointers. The template parameter type in fobj
is int*
. The call to fref
, however, is illegal. When the parameter is a reference, the arrays are not converted to pointers (§ 6.2.4, p. 217). The types of a
and b
don’t match, so the call is in error.
const
conversions and array or function to pointer are the only automatic conversions for arguments to parameters with template types.
A template type parameter can be used as the type of more than one function parameter. Because there are limited conversions, the arguments to such parameters must have essentially the same type. If the deduced types do not match, then the call is an error. For example, our compare
function (§ 16.1.1, p. 652) takes two const T&
parameters. Its arguments must have essentially the same type:
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)
This call is in error because the arguments to compare
don’t have the same type. The template argument deduced from the first argument is long
; the one for the second is int
. These types don’t match, so template argument deduction fails.
If we want to allow normal conversions on the arguments, we can define the function with two type parameters:
// argument types can differ but must be compatible
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
Now the user may supply arguments of different types:
long lng;
flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int)
Of course, a <
operator must exist that can compare values of those types.
A function template can have parameters that are defined using ordinary types—that is, types that do not involve a template type parameter. Such arguments have no special processing; they are converted as usual to the corresponding type of the parameter (§ 6.1, p. 203). For example, consider the following template:
template <typename T> ostream &print(ostream &os, const T &obj)
{
return os << obj;
}
The first function parameter has a known type, ostream&
. The second parameter, obj
, has a template parameter type. Because the type of os
is fixed, normal conversions are applied to arguments passed to os
when print
is called:
print(cout, 42); // instantiates print(ostream&, int)
ofstream f("output");
print(f, 10); // uses print(ostream&, int); converts f to ostream&
In the first call, the type of the first argument exactly matches the type of the first parameter. This call will cause a version of print
that takes an ostream&
and an int
to be instantiated. In the second call, the first argument is an ofstream
and there is a conversion from ofstream
to ostream&
(§ 8.2.1, p. 317). Because the type of this parameter does not depend on a template parameter, the compiler will implicitly convert f
to ostream&
.
Exercises Section 16.2.1
Exercise 16.32: What happens during template argument deduction?
Exercise 16.33: Name two type conversions allowed on function arguments involved in template argument deduction.
Exercise 16.34: Given only the following code, explain whether each of these calls is legal. If so, what is the type of
T
? If not, why not?template <class T> int compare(const T&, const T&);
(a)
compare("hi", "world");
(b)
compare("bye", "dad");
Exercise 16.35: Which, if any, of the following calls are errors? If the call is legal, what is the type of
T
? If the call is not legal, what is the problem?template <typename T> T calc(T, int);
template <typename T> T fcn(T, T);
double d; float f; char c;(a)
calc(c, 'c');
(b)
calc(d, f);
(c)
fcn(c, 'c');
(d)
fcn(d, f);
template <typename T> f1(T, T);
template <typename T1, typename T2) f2(T1, T2);
int i = 0, j = 42, *p1 = &i, *p2 = &j;
const int *cp1 = &i, *cp2 = &j;(a)
f1(p1, p2);
(b)
f2(p1, p2);
(c)
f1(cp1, cp2);
(d)
f2(cp1, cp2);
(e)
f1(p1, cp1);
(f)
f2(p1, cp1);
In some situations, it is not possible for the compiler to deduce the types of the template arguments. In others, we want to allow the user to control the template instantiation. Both cases arise most often when a function return type differs from any of those used in the parameter list.
As an example in which we want to let the user specify which type to use, we’ll define a function template named sum
that takes arguments of two different types. We’d like to let the user specify the type of the result. That way the user can choose whatever precision is appropriate.
We can let the user control the type of the return by defining a third template parameter to represent the return type:
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
In this case, there is no argument whose type can be used to deduce the type of T1
. The caller must provide an explicit template argument for this parameter on each call to sum
.
We supply an explicit template argument to a call the same way that we define an instance of a class template. Explicit template arguments are specified inside angle brackets after the function name and before the argument list:
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
This call explicitly specifies the type for T1
. The compiler will deduce the types for T2
and T3
from the types of i
and lng
.
Explicit template argument(s) are matched to corresponding template parameter(s) from left to right; the first template argument is matched to the first template parameter, the second argument to the second parameter, and so on. An explicit template argument may be omitted only for the trailing (right-most) parameters, and then only if these can be deduced from the function parameters. If our sum
function had been written as
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
then we would always have to specify arguments for all three parameters:
// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);
For the same reasons that normal conversions are permitted for parameters that are defined using ordinary types (§ 16.2.1, p. 680), normal conversions also apply for arguments whose template type parameter is explicitly specified:
long lng;
compare(lng, 1024); // error: template parameters don't match
compare<long>(lng, 1024); // ok: instantiates compare(long, long)
compare<int>(lng, 1024); // ok: instantiates compare(int, int)
As we’ve seen, the first call is in error because the arguments to compare
must have the same type. If we explicitly specify the template parameter type, normal conversions apply. Thus, the call to compare<long>
is equivalent to calling a function taking two const long&
parameters. The int
parameter is automatically converted to long
. In the second call, T
is explicitly specified as int
, so lng
is converted to int
.
Exercises Section 16.2.2
Exercise 16.37: The library
max
function has two function parameters and returns the larger of its arguments. This function has one template type parameter. Could you callmax
passing it anint
and adouble
? If so, how? If not, why not?Exercise 16.38: When we call
make_shared
(§ 12.1.1, p. 451), we have to provide an explicit template argument. Explain why that argument is needed and how it is used.Exercise 16.39: Use an explicit template argument to make it sensible to pass two string literals to the original version of
compare
from § 16.1.1 (p. 652).
Using an explicit template argument to represent a template function’s return type works well when we want to let the user determine the return type. In other cases, requiring an explicit template argument imposes a burden on the user with no compensating advantage. For example, we might want to write a function that takes a pair of iterators denoting a sequence and returns a reference to an element in the sequence:
template <typename It>
??? &fcn(It beg, It end)
{
// process the range
return *beg; // return a reference to an element from the range
}
We don’t know the exact type we want to return, but we do know that we want that type to be a reference to the element type of the sequence we’re processing:
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&
Here, we know that our function will return *beg
, and we know that we can use decltype(*beg)
to obtain the type of that expression. However, beg
doesn’t exist until the parameter list has been seen. To define this function, we must use a trailing return type (§ 6.3.3, p. 229). Because a trailing return appears after the parameter list, it can use the function’s parameters:
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
// process the range
return *beg; // return a reference to an element from the range
}
Here we’ve told the compiler that fcn
’s return type is the same as the type returned by dereferencing its beg
parameter. The dereference operator returns an lvalue (§ 4.1.1, p. 136), so the type deduced by decltype
is a reference to the type of the element that beg
denotes. Thus, if fcn
is called on a sequence of string
s, the return type will be string&
. If the sequence is int
, the return will be int&
.
Sometimes we do not have direct access to the type that we need. For example, we might want to write a function similar to fcn
that returns an element by value (§ 6.3.2, p. 224), rather than a reference to an element.
The problem we face in writing this function is that we know almost nothing about the types we’re passed. In this function, the only operations we know we can use are iterator operations, and there are no iterator operations that yield elements (as opposed to references to elements).
To obtain the element type, we can use a library type transformation template. These templates are defined in the type_traits
header. In general the classes in type_traits
are used for so-called template metaprogramming, a topic that is beyond the scope of this Primer. However, the type transformation templates are useful in ordinary programming as well. These templates are described in Table 16.1 and we’ll see how they are implemented in § 16.5 (p. 710).
Table 16.1. Standard Type Transformation Templates
In this case, we can use remove_reference
to obtain the element type. The remove_reference
template has one template type parameter and a (public)
type member named type
. If we instantiate remove_reference
with a reference type, then type
will be the referred-to type. For example, if we instantiate remove_reference<int&>
, the type
member will be int
. Similarly, if we instantiate remove_reference<string&>, type
will be string
, and so on. More generally, given that beg
is an iterator:
remove_reference<decltype(*beg)>::type
will be the type of the element to which beg
refers: decltype(*beg)
returns the reference type of the element type. remove_reference::type
strips off the reference, leaving the element type itself.
Using remove_reference
and a trailing return with decltype
, we can write our function to return a copy of an element’s value:
// must use typename to use a type member of a template parameter; see § 16.1.3 (p. 670)
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// process the range
return *beg; // return a copy of an element from the range
}
Note that type
is member of a class that depends on a template parameter. As a result, we must use typename
in the declaration of the return type to tell the compiler that type
represents a type (§ 16.1.3, p. 670).
Each of the type transformation templates described in Table 16.1 works similarly to remove_reference
. Each template has a public
member named type
that represents a type. That type may be related to the template’s own template type parameter in a way that is indicated by the template’s name. If it is not possible (or not necessary) to transform the template’s parameter, the type
member is the template parameter type itself. For example, if T
is a pointer type, then remove_pointer<T>::type
is the type to which T
points. If T
isn’t a pointer, then no transformation is needed. In this case, type
is the same type as T
.
Exercises Section 16.2.3
Exercise 16.40: Is the following function legal? If not, why not? If it is legal, what, if any, are the restrictions on the argument type(s) that can be passed, and what is the return type?
template <typename It>
auto fcn3(It beg, It end) -> decltype(*beg + 0)
{
// process the range
return *beg; // return a copy of an element from the range
}Exercise 16.41: Write a version of
sum
with a return type that is guaranteed to be large enough to hold the result of the addition.
When we initialize or assign a function pointer (§ 6.7, p. 247) from a function template, the compiler uses the type of the pointer to deduce the template argument(s).
As an example, assume we have a function pointer that points to a function returning an int
that takes two parameters, each of which is a reference to a const int
. We can use that pointer to point to an instantiation of compare
:
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
The type of the parameters in pf1
determines the type of the template argument for T
. The template argument for T
is int
. The pointer pf1
points to the instantiation of compare
with T
bound to int
. It is an error if the template arguments cannot be determined from the function pointer type:
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?
The problem is that by looking at the type of func
’s parameter, it is not possible to determine a unique type for the template argument. The call to func
could instantiate the version of compare
that takes int
s or the version that takes string
s. Because it is not possible to identify a unique instantiation for the argument to func
, this call won’t compile.
We can disambiguate the call to func
by using explicit template arguments:
// ok: explicitly specify which version of compare to instantiate
func(compare<int>); // passing compare(const int&, const int&)
This expression calls the version of func
that takes a function pointer with two const int&
parameters.
When the address of a function-template instantiation is taken, the context must be such that it allows a unique type or value to be determined for each template parameter.
In order to understand type deduction from a call to a function such as
template <typename T> void f(T &p);
in which the function’s parameter p
is a reference to a template type parameter T
, it is important to keep in mind two points: Normal reference binding rules apply; and const
s are low level, not top level.
When a function parameter is an ordinary (lvalue) reference to a template type parameter (i.e., that has the form T&
), the binding rules say that we can pass only an lvalue (e.g., a variable or an expression that returns a reference type). That argument might or might not have a const
type. If the argument is const
, then T
will be deduced as a const
type:
template <typename T> void f1(T&); // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i); // i is an int; template parameter T is int
f1(ci); // ci is a const int; template parameter T is const int
f1(5); // error: argument to a & parameter must be an lvalue
If a function parameter has type const T&
, normal binding rules say that we can pass any kind of argument—an object (const
or otherwise), a temporary, or a literal value. When the function parameter is itself const
, the type deduced for T
will not be a const
type. The const
is already part of the function parameter type; therefore, it does not also become part of the template parameter type:
template <typename T> void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i); // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5); // a const & parameter can be bound to an rvalue; T is int
When a function parameter is an rvalue reference (§ 13.6.1, p. 532) (i.e., has the form T&&)
, normal binding rules say that we can pass an rvalue to this parameter. When we do so, type deduction behaves similarly to deduction for an ordinary lvalue reference function parameter. The deduced type for T
is the type of the rvalue:
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
Assuming i
is an int
object, we might think that a call such as f3(i)
would be illegal. After all, i
is an lvalue, and normally we cannot bind an rvalue reference to an lvalue. However, the language defines two exceptions to normal binding rules that allow this kind of usage. These exceptions are the foundation for how library facilities such as move
operate.
The first exception affects how type deduction is done for rvalue reference parameters. When we pass an lvalue (e.g., i
) to a function parameter that is an rvalue reference to a template type parameter (e.g, T&&)
, the compiler deduces the template type parameter as the argument’s lvalue reference type. So, when we call f3(i)
, the compiler deduces the type of T
as int&
, not int
.
Deducing T
as int&
would seem to mean that f3
’s function parameter would be an rvalue reference to the type int&
. Ordinarily, we cannot (directly) define a reference to a reference (§ 2.3.1, p. 51). However, it is possible to do so indirectly through a type alias (§ 2.5.1, p. 67) or through a template type parameter.
In such contexts, we see the second exception to the normal binding rules: If we indirectly create a reference to a reference, then those references “collapse.” In all but one case, the references collapse to form an ordinary lvalue reference type. The new standard, expanded the collapsing rules to include rvalue references. References collapse to form an rvalue reference only in the specific case of an rvalue reference to an rvalue reference. That is, for a given type X
:
•
X& &, X& &&
, andX&& &
all collapse to typeX&
• The type
X&& &&
collapses toX&&
Reference collapsing applies only when a reference to a reference is created indirectly, such as in a type alias or a template parameter.
The combination of the reference collapsing rule and the special rule for type deduction for rvalue reference parameters means that we can call f3
on an lvalue. When we pass an lvalue to f3
’s (rvalue reference) function parameter, the compiler will deduce T
as an lvalue reference type:
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
When a template parameter T
is deduced as a reference type, the collapsing rule says that the function parameter T&&
collapses to an lvalue reference type. For example, the resulting instantiation for f3(i)
would be something like
// invalid code, for illustration purposes only
void f3<int&>(int& &&); // when T is int&, function parameter is int& &&
The function parameter in f3
is T&&
and T
is int&
, so T&&
is int& &&
, which collapses to int&
. Thus, even though the form of the function parameter in f3
is an rvalue reference (i.e., T&&)
, this call instantiates f3
with an lvalue reference type (i.e., int&)
:
void f3<int&>(int&); // when T is int&, function parameter collapses to int&
There are two important consequences from these rules:
• A function parameter that is an rvalue reference to a template type parameter (e.g.,
T&&)
can be bound to an lvalue; and
• If the argument is an lvalue, then the deduced template argument type will be an lvalue reference type and the function parameter will be instantiated as an (ordinary) lvalue reference parameter (
T&
)
It is also worth noting that by implication, we can pass any type of argument to a T&&
function parameter. A parameter of such a type can (obviously) be used with rvalues, and as we’ve just seen, can be used by lvalues as well.
An argument of any type can be passed to a function parameter that is an rvalue reference to a template parameter type (i.e.,
T&&)
. When an lvalue is passed to such a parameter, the function parameter is instantiated as an ordinary, lvalue reference (T&
).
The fact that the template parameter can be deduced to a reference type can have surprising impacts on the code inside the template:
template <typename T> void f3(T&& val)
{
T t = val; // copy or binding a reference?
t = fcn(t); // does the assignment change only t or val and t?
if (val == t) { /* ... */ } // always true if T is a reference type
}
When we call f3
on an rvalue, such as the literal 42, T
is int
. In this case, the local variable t
has type int
and is initialized by copying the value of the parameter val
. When we assign to t
, the parameter val
remains unchanged.
On the other hand, when we call f3
on the lvalue i
, then T
is int&
. When we define and initialize the local variable t
, that variable has type int&
. The initialization of t
binds t
to val
. When we assign to t
, we change val
at the same time. In this instantiation of f3
, the if
test will always yield true
.
It is surprisingly hard to write code that is correct when the types involved might be plain (nonreference) types or reference types (although the type transformation classes such as remove_reference
can help (§ 16.2.3, p. 684)).
In practice, rvalue reference parameters are used in one of two contexts: Either the template is forwarding its arguments, or the template is overloaded. We’ll look at forwarding in § 16.2.7 (p. 692) and at template overloading in § 16.3 (p. 694).
For now, it’s worth noting that function templates that use rvalue references often use overloading in the same way as we saw in § 13.6.3 (p. 544):
template <typename T> void f(T&&); // binds to nonconst rvalues
template <typename T> void f(const T&); // lvalues and const rvalues
As with nontemplate functions, the first version will bind to modifiable rvalues and the second to lvalues or to const
rvalues.
Exercises Section 16.2.5
Exercise 16.42: Determine the type of
T
and ofval
in each of the following calls:template <typename T> void g(T&& val);
int i = 0; const int ci = i;(a)
g(i);
(b)
g(ci);
(c)
g(i * ci);
Exercise 16.43: Using the function defined in the previous exercise, what would the template parameter of
g
be if we calledg(i = ci)?
Exercise 16.44: Using the same three calls as in the first exercise, determine the types for
T
ifg
’s function parameter is declared asT
(notT&&)
. What ifg
’s function parameter isconst T&?
Exercise 16.45: Given the following template, explain what happens if we call
g
on a literal value such as 42. What if we callg
on a variable of typeint
?template <typename T> void g(T&& val) { vector<T> v; }
std::move
The library move
function (§ 13.6.1, p. 533) is a good illustration of a template that uses rvalue references. Fortunately, we can use move
without understanding the template mechanisms that it uses. However, looking at how move
works can help cement our general understanding, and use, of templates.
In § 13.6.2 (p. 534) we noted that although we cannot directly bind an rvalue reference to an lvalue, we can use move
to obtain an rvalue reference bound to an lvalue. Because move
can take arguments of essentially any type, it should not be surprising that move
is a function template.
std::move
Is DefinedThe standard defines move
as follows:
// for the use of typename in the return type and the cast see § 16.1.3 (p. 670)
// remove_reference is covered in § 16.2.3 (p. 684)
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
// static_cast covered in § 4.11.3 (p. 163)
return static_cast<typename remove_reference<T>::type&&>(t);
}
This code is short but subtle. First, move
’s function parameter, T&&
, is an rvalue reference to a template parameter type. Through reference collapsing, this parameter can match arguments of any type. In particular, we can pass either an lvalue or an rvalue to move
:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value
std::move
WorksIn the first assignment, the argument to move
is the rvalue result of the string
constructor, string("bye")
. As we’ve seen, when we pass an rvalue to an rvalue reference function parameter, the type deduced from that argument is the referred-to type (§ 16.2.5, p. 687). Thus, in std::move(string("bye!"))
:
• The deduced type of
T
isstring
.
• Therefore,
remove_reference
is instantiated withstring
.
• The
type
member ofremove_reference<string>
isstring
.
• The return type of
move
isstring&&
.
•
move
’s function parameter,t
, has typestring&&
.
Accordingly, this call instantiates move<string>
, which is the function
string&& move(string &&t)
The body of this function returns static_cast<string&&>(t)
. The type of t
is already string&&
, so the cast does nothing. Therefore, the result of this call is the rvalue reference it was given.
Now consider the second assignment, which calls std::move(s1)
. In this call, the argument to move
is an lvalue. This time:
• The deduced type of
T
isstring&
(reference tostring
, not plainstring
).
• Therefore,
remove_reference
is instantiated withstring&
.
• The
type
member ofremove_reference<string&>
isstring
,
• The return type of
move
is stillstring&&
.
•
move
’s function parameter,t
, instantiates asstring& &&
, which collapses tostring&
.
Thus, this call instantiates move<string&>
, which is
string&& move(string &t)
and which is exactly what we’re after—we want to bind an rvalue reference to an lvalue. The body of this instantiation returns static_cast<string&&>(t)
. In this case, the type of t
is string&
, which the cast converts to string&&
.
static_cast
from an Lvalue to an Rvalue Reference Is PermittedOrdinarily, a static_cast
can perform only otherwise legitimate conversions (§ 4.11.3, p. 163). However, there is again a special dispensation for rvalue references: Even though we cannot implicitly convert an lvalue to an rvalue reference, we can explicitly cast an lvalue to an rvalue reference using static_cast
.
Binding an rvalue reference to an lvalue gives code that operates on the rvalue reference permission to clobber the lvalue. There are times, such as in our StrVec reallocate
function in § 13.6.1 (p. 533), when we know it is safe to clobber an lvalue. By letting us do the cast, the language allows this usage. By forcing us to use a cast, the language tries to prevent us from doing so accidentally.
Finally, although we can write such casts directly, it is much easier to use the library move
function. Moreover, using std::move
consistently makes it easy to find the places in our code that might potentially clobber lvalues.
Exercises Section 16.2.6
Exercise 16.46: Explain this loop from
StrVec::reallocate
in § 13.5 (p. 530):for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
Some functions need to forward one or more of their arguments with their types unchanged to another, forwarded-to, function. In such cases, we need to preserve everything about the forwarded arguments, including whether or not the argument type is const
, and whether the argument is an lvalue or an rvalue.
As an example, we’ll write a function that takes a callable expression and two additional arguments. Our function will call the given callable with the other two arguments in reverse order. The following is a first cut at our flip function:
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
This template works fine until we want to use it to call a function that has a reference parameter:
void f(int v1, int &v2) // note v2 is a reference
{
cout << v1 << " " << ++v2 << endl;
}
Here f
changes the value of the argument bound to v2
. However, if we call f
through flip1
, the changes made by f
do not affect the original argument:
f(42, i); // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
The problem is that j
is passed to the t1
parameter in flip1
. That parameter has is a plain, nonreference type, int
, not an int&
. That is, the instantiation of this call to flip1
is
void flip1(void(*fcn)(int, int&), int t1, int t2);
The value of j
is copied into t1
. The reference parameter in f
is bound to t1
, not to j
.
To pass a reference through our flip function, we need to rewrite our function so that its parameters preserve the “lvalueness” of its given arguments. Thinking ahead a bit, we can imagine that we’d also like to preserve the const
ness of the arguments as well.
We can preserve all the type information in an argument by defining its corresponding function parameter as an rvalue reference to a template type parameter. Using a reference parameter (either lvalue or rvalue) lets us preserve const
ness, because the const
in a reference type is low-level. Through reference collapsing (§ 16.2.5, p. 688), if we define the function parameters as T1&&
and T2&&
, we can preserve the lvalue/rvalue property of flip’s arguments (§ 16.2.5, p. 687):
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
As in our earlier call, if we call flip2(f, j, 42)
, the lvalue j
is passed to the parameter t1
. However, in flip2
, the type deduced for T1
is int&
, which means that the type of t1
collapses to int&
. The reference t1
is bound to j
. When flip2
calls f
, the reference parameter v2
in f
is bound to t1
, which in turn is bound to j
. When f
increments v2
, it is changing the value of j
.
A function parameter that is an rvalue reference to a template type parameter (i.e.,
T&&
) preserves theconst
ness and lvalue/rvalue property of its corresponding argument.
This version of flip2
solves one half of our problem. Our flip2
function works fine for functions that take lvalue references but cannot be used to call a function that has an rvalue reference parameter. For example:
void g(int &&i, int& j)
{
cout << i << " " << j << endl;
}
If we try to call g
through flip2
, we will be passing the parameter t2
to g
’s rvalue reference parameter. Even if we pass an rvalue to flip2
:
flip2(g, i, 42); // error: can't initialize int&& from an lvalue
what is passed to g
will be the parameter named t2
inside flip2
. A function parameter, like any other variable, is an lvalue expression (§ 13.6.1, p. 533). As a result, the call to g
in flip2
passes an lvalue to g
’s rvalue reference parameter.
std::forward
to Preserve Type Information in a CallWe can use a new library facility named forward
to pass flip2
’s parameters in a way that preserves the types of the original arguments. Like move, forward
is defined in the utility
header. Unlike move, forward
must be called with an explicit template argument (§ 16.2.2, p. 682). forward
returns an rvalue reference to that explicit argument type. That is, the return type of forward<T>
is T&&
.
Ordinarily, we use forward
to pass a function parameter that is defined as an rvalue reference to a template type parameter. Through reference collapsing on its return type, forward
preserves the lvalue/rvalue nature of its given argument:
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
Here we use Type
—which is deduced from arg
—as forward
’s explicit template argument type. Because arg
is an rvalue reference to a template type parameter, Type
will represent all the type information in the argument passed to arg
. If that argument was an rvalue, then Type
is an ordinary (nonreference) type and forward<Type>
will return Type&&
. If the argument was an lvalue, then—through reference collapsing—Type
itself is an lvalue reference type. In this case, the return type is an rvalue reference to an lvalue reference type. Again through reference collapsing—this time on the return type—forward<Type>
will return an lvalue reference type.
When used with a function parameter that is an rvalue reference to template type parameter (
T&&
),forward
preserves all the details about an argument’s type.
Using forward
, we’ll rewrite our flip function once more:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
If we call flip(g, i, 42), i
will be passed to g
as an int&
and 42
will be passed as an int&&
.
As with
std::move
, it’s a good idea not to provide ausing
declaration forstd::forward
. § 18.2.3 (p. 798) will explain why.