【C++ Primer】第16章 模板与泛型编程 (2)


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_referenceX&X&&
其他
X
T
add_constX&const X 或函数
其他
T
const T
add_lvalue_referenceX&
X&&
其他
T
X&
T&
add_rvalue_referenceX&X&&
其他
T
T&&
remove_pointerX*
其他
X
T
add_pointerX&X&&
其他
X*
T*
make_signedunsigned X
其他
X
T
make_unsigned带符号类型
其他
unsigned T
T
remove_extentX[n]
其他
X
T
remove_all_extentsX[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 声明。


【C++ Primer】目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值