Part III: Tools for Class Authors
Chapter 16. Templates and Generic Programming
16.2 模板实参推断
默认情况下,编译器使用调用中的实参来确定函数模板的模板形参。
从函数实参来确定模板实参的过程称为模板实参推断 (template argument deduction)。
在模板实参推断期间,编译器使用调用中的实参查找模板实参,生成与给定调用最匹配的函数版本。
类型转换与模板类型形参
在调用中传递给函数模板的实参用于初始化模板形参。如果函数形参的类型使用模板类型形参,那么它具有特殊的初始化规则。只有极少数的类型转换会自动应用于此类实参。编译器将生成新的实例,而不是对实参进行类型转换。
顶层 const 在形参或实参中都会被忽略。在函数模板实参调用中,执行的其他类型转换只包括:
- const 转换:函数形参如果是 const 引用(或指针),则可以传递一个非 const 引用(或指针)对象。
- 数组或函数到指针的转换:如果函数形参不是引用类型,那么正常的指针转换会应用于数组或函数类型的实参。数组实参会转换为指向其首元素的指针。函数实参会转换为指向函数类型的指针。
其他类型转换,比如算术转换、派生类到基类的转换、用户定义的转换等,都不会执行。
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
在第二对调用中,传递数组实参,数组大小不同,因此是不同的类型。
在 fobj 调用中,数组类型不同没有关系。两个数组都会转换为指针。
但 fref 调用不合法。当形参是引用时,数组不会转换为指针。a 和 b 的类型不匹配,所以调用错误。
使用相同模板形参类型的函数形参
一个模板类型形参可用作多个函数形参。因为只允许有限的类型转换,这种形参的实参必须基本相同。
compare 函数接受两个 const T& 形参,它的实参必须具有基本相同的类型:
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)
如果想要允许实参之间正常的类型转换,可以使用两种类型形参定义函数:
// 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;
}
long lng;
flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int)
正常类型转换应用于普通实参
函数模板的形参可以使用普通类型定义,即形参类型不涉及模板类型形参。这样的实参没有特殊处理,它们照常转换为形参的相应类型。
template <typename T> ostream &print(ostream &os, const T &obj) {
return os << obj;
}
因为 os 类型固定,所以当调用 print 时正常的类型转换可以应用于传递给 os 的实参。
print(cout, 42); // instantiates print(ostream&, int)
ofstream f("output");
print(f, 10); // uses print(ostream&, int); converts f to ostream&
函数模板显式实参
在一些情况下,编译器不能推断出模板实参的类型。在另外一些情况下,我们希望允许用户控制模板实例化。当函数返回类型于形参列表中的都不同时,这两种情况最常出现。
指定显式模板实参
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
提供调用的显式模板实参的方式于定义类模板的示例相同。
// 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)
显式模板实参从左到右与对应的模板形参匹配;第一个模板实参与第一个模板形参匹配,第二个实参与第二个形参匹配,依此类推。仅对于尾部(最右边)的形参,且可以从函数形参中推断出类型,才可以省略显式模板实参。
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1);
// 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);
正常类型转换应用于显式指定的实参
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)
尾置返回类型与类型转换
template <typename It>
??? &fcn(It beg, It end) {
// process the range
return *beg; // return a reference to an element from the range
}
我们不知道返回结果的确切类型,但知道所需类型是处理序列元素类型的引用。
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&
在C++11标准中,为了定义这个函数,必须使用尾置返回类型。因为尾置返回出现在形参列表后面,所以可以使用函数的形参。
// 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
}
解引用运算符返回左值,所以由 decltype 推断出的类型是 beg 指向元素类型的引用。
类型转换标准库模板类
有时无法直接获取所需的类型。例如,写一个于 fcn 类似的函数,它返回元素值,而不是元素的引用。
为了获取元素类型,可以使用标准库类型转换 (type transformation) 模板。这些模板定义在 type_traits
头文件中。
通常,type_traits 中的类用于所谓的模板元编程。但是,类型转换模板在普通编程中也很有用。
表16.1 标准类型转换模板
对于 Mod<T> ,其中 Mod 是 | 如果 T 是 | 那么 Mod<T>::type 是 |
---|---|---|
remove_reference | X& 或 X&& 其他 | X T |
add_const | X& 、const X 或函数 其他 | T const T |
add_lvalue_reference | X& X&& 其他 | T X& T& |
add_rvalue_reference | X& 或 X&& 其他 | T T&& |
remove_pointer | X* 其他 | X T |
add_pointer | X& 或 X&& 其他 | X* T* |
make_signed | unsigned X 其他 | X T |
make_unsigned | 带符号类型 其他 | unsigned T T |
remove_extent | X[n] 其他 | X T |
remove_all_extents | X[n1][n2]... 其他 | X T |
在本例中,可以使用 remove_reference 获取元素类型。remove_reference 模板有一个模板类型形参,和一个 public 类型成员 type。如果使用引用类型实例化 remove_reference,那么 type 是被引用的类型。
// must use typename to use a type member of a template parameter; see § 16.1
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
}
注意 type 是一个依赖于模板形参的类的成员。因此必须在返回类型的声明中使用 typename,告诉编译器 type 表示一个类型。
函数指针与实参推断
当从函数模板中初始化或赋值一个函数指针时,编译器使用指针的类型来推断模板的实参。
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;
pf1 中的形参类型决定了 T 的模板实参类型。T 的模板实参是 int。
如果模板实参类型不能由函数指针类型确定,程序会出错。
// 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?
问题在于,通过查看 func 形参的类型,无法确定模板实参的唯一类型。
可以使用显式模板实参来消除 func 调用的二义性。
// ok: explicitly specify which version of compare to instantiate
func(compare<int>); // passing compare(const int&, const int&)
模板实参推断与引用
为了理解从函数调用中的类型推断,举例
template <typename T> void f(T &p);
函数形参 p 是模板类型形参 T 的引用,记住两点:应用正常的引用绑定规则;const 是底层的,不是顶层的。
从左值引用函数形参的类型推断
当函数形参是模板类型形参的左值引用(即形式为 T&)时,绑定规则说明,只能传递左值(即,变量或返回引用的表达式)。实参可能是 const 类型,也可能不是。如果实参是 const,那么 T 会被推断为 const。
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
如果函数形参类型是 const T&,正常的绑定规则说明,可以传递任何类型的实参 —— 对象(const 或其他)、临时对象、或字面常量值等。当函数形参本身是 const 时,T 推断的类型不会是 const。const 已经是函数形参类型的一部分,因此,它不会成员模板形参类型的一部分。
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
从右值引用函数形参的类型推断
当函数形参是右值引用(即形式为 T&&)时,正常绑定规则说明,可以向这个形参传递右值。类型推断行为与左值引用函数形参的推断相似。推断 T 的类型是右值的类型。
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
引用折叠与右值引用形参
C++语言对于正常绑定规则定义了两个例外。这些例外是如 move 操作这类库设施的基础。
第一个例外影响右值引用类型形参是如何进行类型推断的。如果一个函数形参是模板类型形参的右值引用 (即 T&&),当传递一个左值给这个函数形参时,编译器推断模板类型形参是实参的左值引用类型。
因此,设 i 是 int 对象,当调用 f3(i) 时,编译器推断 T 的类型是 int&,而不是 int。
推断 T 是 int& 看起来意味着 f3 的函数形参是类型 int& 的左值引用。
正常规则的第二个例外:如果间接创建了一个引用的引用,那么这些引用“折叠”。在C++11标准中,将折叠规则扩展到右值引用。对于给定的类型 X,
X& &
、X& &&
和X&& &
都折叠成类型X&
- 类型
X&& &&
重叠成X&&
注:引用折叠只引用于间接创建的引用的引用,例如在类型别名或模板形参中。
引用折叠规则与右值引用形参的类型推断的特殊规则相结合,意味着可以在左值上调用 f3。
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
根据这些规则,有两个重要结果:
- 如果一个函数形参是指向模板类型形参的左值引用,那么它可以绑定一个左值;并且
- 如果实参是左值,那么推断的模板实参类型是左值引用类型,函数形参实例化为左值引用形参 (T&)。
注意,这蕴含着:可以将任何类型的实参传递给 T&& 函数形参。
编写带有右值引用形参的模板函数
模板形参可以推断为引用类型,这一事实影响了模板内的代码:
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
}
当代码中涉及的类型可能是非引用类型,也可能是引用类型时,编写正确的代码变得异常困难。
在实际中,右值引用用于两种情况:模板转发其实参,或模板被重载。
使用右值引用的函数模板通常使用下面的重载方式:
template <typename T> void f(T&&); // binds to nonconst rvalues
template <typename T> void f(const T&); // lvalues and const rvalues
与非模板函数一样,第一个版本将绑定到可修改的右值,第二个版本将绑定到左值或 const 右值。
理解 std::move
标准库 move 函数是使用右值引用的模板的一个好的例子。
虽然不能直接将一个左值引用绑定到一个右值上,但可以使用 move 获取一个绑定到右值的右值引用。
std::move 是如何定义的
// for the use of typename in the return type and the cast see § 16.1
// remove_reference is covered in § 16.2
template <typename T> typename remove_reference<T>::type&& move(T&& t) {
// static_cast covered in § 4.11
return static_cast<typename remove_reference<T>::type&&>(t);
}
move 的函数形参 T&& 是一个指向模板形参类型的右值引用。通过引用折叠,该形参可以匹配任何类型的实参。
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 是如何工作的
在第一个赋值中,move 的实参是 string 构造函数的右值结果。当向右值引用函数形参中传递右值时,从实参中推断出的类型是被引用的类型。在 std::move(string(“bye!”)) 中:
- 推断出的 T 的类型是 string。
- 因此,remove_reference 使用 string 进行实例化。
- remove_reference<string> 的 type 成员是 string。
- move 的返回类型是 string&&。
- move 的函数形参 t 的类型是 string&&。
因此,该调用实例化 move<string>,即函数
string&& move(string &&t)
该函数体返回 static_cast<string&&>(t),而 t 的类型已经是 string&&,该调用的结果就是给定的右值引用。
第二个赋值调用 std::move(s1)。该调用中 move 的实参是一个左值。这次:
- 推断出的 T 的类型是 string&。
- 因此,remove_reference 使用 string& 进行实例化。
- remove_reference<string&> 的 type 成员是 string。
- move 的返回类型仍然是 string&&。
- move 的函数形参 t 实例化为 string& &&,折叠成 string&。
因此,该调用实例化 move<string&>,即
string&& move(string &t)
该实例的函数体返回 static_cast<string&&>(t)。在本例中,t 的类型的 string&,将其强制转换为 string&&。
从一个左值 static_cast 到一个右值引用是允许的
通常,static_cast 只能执行其他合法转换(第4.11节)。但是,对于右值引用又有一种特许方式:虽然不能将左值隐式地转换为右值引用,但可以使用 static_cast 将左值显式地转换为右值引用。
转发
一些函数需要将一个或多个实参转发给其他函数,实参类型不变。在这种情况下,需要保留转发实参的所有信息,包括实参类型是否是 const,或是左值还是右值等。
// 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);
}
这个模板工作良好,直到使用它调用拥有一个引用形参的函数:
void f(int v1, int &v2) // note v2 is a reference
{
cout << v1 << " " << ++v2 << endl;
}
上面 f 改变了绑定到 v2 的实参的值。但是,如果通过 flip1 调用 f,f 所做的改变不会影响到原来的实参:
f(42, i); // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
问题是 j 被传递给 flip1 中的 t1 形参。这个形参是一个普通的、非引用类型 int,不是 int&。即这个 flip1 调用的实例化是
void flip1(void(*fcn)(int, int&), int t1, int t2);
j 的值复制到 t1。f 中的引用形参绑定到 t1,不是 j。
定义保留类型信息的函数形参
为了通过 flip 函数传递引用,需要重写函数以使它的形参能够保留其给定实参的“左值性”。更进一步,希望还能保留实参的 const 属性。
通过将实参对应的函数形参定义为对模板类型形参的右值引用,可以保留实参中的所有类型信息。使用引用形参(左值或右值)可以保留 const 属性,因为引用类型中的 const 是低级的。通过引用折叠,如果将函数形参定义为 T1&& 和 T2&&,则可以保留 flip 实参的左值/右值属性:
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
在 flip2 中,推断出的 T1 的类型是 int&,意味着 t1 的类型折叠成 int&。引用 t1 绑定到 j。当 flip2 调用 f 时,f 中的引用形参 v2 绑定到 t1,t1 又绑定到 j。当 f 递增 v2 时,它改变了 j 的值。
注:如果某个函数形参是模板类型形参的右值引用 (即 T&&),那么它保留了其对应实参的 const 和左值/右值属性。
此版本的 flip2 解决了一半问题。对于接受左值引用的函数,flip2 函数工作良好,但不能用于具有右值引用形参的函数调用。
void g(int &&i, int& j) {
cout << i << " " << j << endl;
}
如果试图通过 flip2 调用 g,那么形参 t2 将被传递给 g 的右值引用形参。即使传递一个右值给 flip2:
flip2(g, i, 42); // error: can't initialize int&& from an lvalue
传递给 g 的是 flip2 中的形参 t2。与其他变量类似,函数形参是左值表达式 (第13.6节)。因此,flip2 中 g 的调用将左值传递给 g 的右值引用形参。
使用 std::forward 保留调用中的类型信息
可以使用C++11标准库设施 forward
以保留原始实参类型的方式,传递 flip2 的形参。
forward 定义在 utility 头文件中,必须显式模板实参来调用。
forward 返回该显式实参类型的右值引用。即 forward<T>
的返回类型 T&&
。
通常情况下,使用 forward 传递定义成模板类型形参的右值引用的函数形参。通过其返回类型上的引用折叠,forward 保留其给定实参的左值/右值属性。
template <typename Type> intermediary(Type &&arg) {
finalFcn(std::forward<Type>(arg));
// ...
}
上面使用 Type 作为 forward 显式模板实参类型,由 arg 推断出来。因为 arg 是模板类型形参的右值引用,Type 表示传递给 arg 实参中的所有类型信息。
如果实参是一个右值,那么 Type 是非引用类型,forward<Type> 将返回 Type&&。
如果实参是一个左值,那么通过引用折叠,Type 本身是左值引用类型。这样返回类型是指向左值引用类型的右值引用。在返回类型上进行引用折叠,forward<Type> 将返回左值引用类型。
注:当用于函数形参 T&& 时,forward 保留实参类型的所有细节。
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
如果调用 flip(g,i,42)
,i 将以 int& 传递给 g,42 以 int&& 传递。
注:与 std::move 类似,对于 std::forward,最好不要提供 using 声明。