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


Part III: Tools for Class Authors
Chapter 16. Templates and Generic Programming


16.3 重载与模板

函数模板可以被其他模板或普通的非模板函数重载。与往常一样,具有相同名称的函数必须在形参的数量或类型上有所不同。

☛函数匹配会受到函数模板存在的影响:

  • 调用的候选函数包括模板实参推断 (第16.2节) 成功的函数模板实例化。
  • 候选函数模板总是可行的,因为模板实参推断会排除不可行的模板。
  • 可行函数 (模板与非模板) 按照调用时需要的类型转换进行排序。当然,用于调用函数模板的类型转换非常有限。
  • 如果恰好有一个函数提供的匹配比其他函数都好,则选择此函数。但是,如果有多个函数提供同样好的匹配,那么:
    如果同样好的匹配集合中,只有一个非模板函数,那么调用该非模板函数。
    如果集合中没有非模板函数,但有多个函数模板,且其中一个模板比其他更特例化,则调用该函数模板。
    否则,调用具有二义性。

⚠警告:正确定义重载函数模板集合,需要深刻理解类型之间的关系,以及应用于模板函数实参的类型转换。

编写重载模板

示例:构造一组函数来帮助调试,命名为 debug_rep,每个都将返回给定对象的 string 表示。

首先编写该函数最通用版本,将其定义成模板,接受一个 const 对象的引用:

// print any type we don't otherwise handle
template <typename T> string debug_rep(const T &t) {
	ostringstream ret; // see § 8.3
	ret << t; // uses T's output operator to print a representation of t
	return ret.str(); // return a copy of the string to which ret is bound
}

该函数用于生成一个 string,对应于具有输出运算符的类型的对象。

接下来,定义一个 debug_rep 版本打印指针:

// print pointers as their pointer value, followed by the object to which the pointer points
// NB: this function will not work properly with char*; see § 16.3
template <typename T> string debug_rep(T *p) {
	ostringstream ret;
	ret << "pointer: " << p;         // print the pointer's own value
	if (p)
		ret << " " << debug_rep(*p); // print the value to which p points
	else
		ret << " null pointer";      // or indicate that the p is null
	return ret.str(); // return a copy of the string to which ret is bound
}

注意该函数不能用于打印字符指针,因为 IO 库为 char* 定义了一个 << 版本。该版本的 << 假定该指针指向一个以空字符结尾的字符串数组,打印的是数组的内容,而不是它的地址。

string s("hi");
cout << debug_rep(s) << endl;

对于该调用,只有第一个版本的 debug_rep 是可行的,故调用该函数。第二个版本需要一个指针形参,无法从非指针实参上实例化一个期待指针类型的函数模板,因此实参推断失败。

如果使用指针调用 debug_rep:

cout << debug_rep(&s) << endl;

两个函数都能生成可行的示例:

  • debug_rep(const string* &),第一个 debug_rep 版本的示例,其中 T 绑定到 string*
  • debug_rep(string*),第二个 debug_rep 版本的示例,其中 T 绑定到 string

第二个 debug_rep 版本的实例是该调用的精确匹配。第一个版本的实例需要从普通指针到指向 const 指针的类型转换。正常匹配规则说明应该选择第二个模板。

多个可行模板

const string *sp = &s;
cout << debug_rep(sp) << endl; 

本例中两个模板都可行,且两个都提供精确匹配:

  • debug_rep(const string* &),第一个版本的实例,T 绑定到 const string*
  • debug_rep(const string*),第一个版本的实例,T 绑定到 const string

正常函数匹配不能区分这两个调用。但是,根据重载函数模板的特殊规则,该调用解析为 debug_rep(T*),这是更特例化的模板。

这个规则的原因在于,没有它,将无法在指向 const 的指针上调用 debug_rep 的指针版本。
问题在于模板 debug_rep(const T&) 基本上可以在任何类型上调用,包括指针类型。这个模板比 debug_rep(T*) 更通用,而后者只能在指针类型上调用。
没有这条规则,传递指向 const 的指针的调用总是具有二义性的。

非模板与模板重载

定义一个 debug_rep 的普通非模板版本:

// print strings inside double quotes
string debug_rep(const string &s) {
	return '"' + s + '"';
}

在 string 上调用 debug_rep:

string s("hi");
cout << debug_rep(s) << endl;

现有两个同样好的可行函数:

  • debug_rep<string>(const string&),第一个模板,T 绑定 string
  • debug_rep(const string&),非模板函数

当存在多个同样好的函数模板时,选择最特例化的那个,处于同样原因,相比于同等匹配的函数模板,首选非函数模板。

重载模板与类型转换

考虑情况:指向C风格字符串的指针与字符串字面常量。

cout << debug_rep("hi world!") << endl; // calls debug_rep(T*)

三种 debug_rep 函数都是可行的:

  • debug_rep(const T&),T 绑定 char[10]
  • debug_rep(T*),T 绑定 const char
  • debug_rep(const string&),需要从 const char* 到 string 的类型转换

对于给定实参,两个模板都能提供精确匹配——第二个模板需要一次(许可的)从数组到指针的类型转换,而这种类型转换被认为是精确匹配。
非模板版本是可行的,但需要一次用户定义的类型转换。因此,不比精确匹配好。
最终,因为 T* 更特例化,所以选择它。

如果想要像 string 那样处理字符指针,可以定义另外两个非模板重载版本:

// convert the character pointers to string and call the string version of debug_rep
string debug_rep(char *p) {
	return debug_rep(string(p));
}
string debug_rep(const char *p) {
	return debug_rep(string(p));
}

缺少声明可能会导致程序行为异常

注意,为了让 char* 版本的 debug_rep 工作正常,在定义该函数时,debug_rep(const string&) 必须在作用域中。

template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// the following declaration must be in scope
// for the definition of debug_rep(char*) to do the right thing
string debug_rep(const string &);
string debug_rep(char *p) {
	// if the declaration for the version that takes a const string& is not in scope
	// the return will call debug_rep(const T&) with T instantiated to string
	return debug_rep(string(p));
}

💡Tip:在定义重载函数之前,声明重载集合中的每个函数。这样就不必担心编译器在遇到要调用的函数之前是否会实例化调用。


16.4 可变参数模板

可变参数模板 (variadic template) 是可以接受可变数量形参的模板函数或模板类。
可变数量的形参被称为参数包 (parameter pack)。
参数包有两种:模板参数包 (template parameter pack) 代表零个或多个模板参数,而函数参数包 (function parameter pack) 代表零个或多个函数参数。

使用省略号指出模板形参或函数形参表示包。
在模板形参列表中,class...typename... 指出下面的形参表示零个或多个类型的列表;类型名字后面接省略号表示给定类型的零个或多个非类型形参的列表。
在函数形参列表中,如果某个形参的类型是模板参数包,那么该形参为函数参数包。

// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

声明了 foo 是一个可变参数函数,它有一个名为类型形参 T 和一个模板参数包 Args。这个包表示零个或多个额外的类型形参。foo 的函数形参列表有一个类型为 const & 的形参,指向类型 T,以及一个函数参数包 rest。这个包表示零个或多个函数形参。

编译器从函数实参中推断模板形参类型。对于可变参数模板,编译器还会推断包中的形参个数。

int i = 0; double d = 3.14;
string s = "how now brown cow";
foo(i, s, 42, d);    // three parameters in the pack
foo(s, 42, "hi");    // two parameters in the pack
foo(d, s);           // one parameter in the pack
foo("hi");           // empty pack

编译器会实例化 4 种不同的实例:

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

sizeof… 运算符

可以使用 sizeof... 运算符获取包中的元素个数。

template<typename ... Args>
void g(Args ... args) {
	cout << sizeof...(Args) << endl;  // number of type parameters
	cout << sizeof...(args) << endl;  // number of function parameters
}

编写可变参数函数模板

如6.2节所述,可以使用 initializer_list 定义一个接受可变数目实参的函数。但是,实参必须是同一类型 (或者可以转换为一个公共类型)。当不知道想要处理的实参个数和类型时,可以使用可变参数函数。

可变参数函数常常是递归的。第一个调用处理包中的第一个实参,并在剩余实参上调用自身。

// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t) {
	return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) {
	os << t << ", ";           // print the first argument
	return print(os, rest...); // recursive call; print the other arguments
}

给定:

print(cout, i, s, 42);  // two parameters in the pack

上面的递归执行如下:

调用trest…
print(cout, i, s, 42)is, 42
print(cout, s, 42)s42
print(cout, 42)调用 print 的非可变参数版本

对于递归的最后一次调用 print(cout, 42),两种版本的 print 都是可行的。
对于该调用,两个函数提供同样好的匹配。但是,非可变参数版本比可变参数版本更特例化,故对于该调用,选择非可变参数版本。

⚠当定义 print 的可变参数版本时,非可变参数版本的声明必须在作用域中。否则,可变参数函数会无限递归。

包扩展

对于一个参数包,除了获取其大小以外,可以对它进行的唯一其他操作就是扩展 (expand) 它。
当扩展一个包时,还提供用于每个扩展元素的模式 (pattern)。扩展一个包是将这个包分成其组成元素,并在这样做时将模式应用于每个元素。可通过在模式的右边放置一个省略号 ... 来触发扩展。

例如,print 函数包含两个扩展。

template <typename T, typename... Args>
ostream & print(ostream &os, const T &t, const Args&... rest)// expand Args
{
	os << t << ", ";
	return print(os, rest...);                     // expand rest
}

第一个扩展将扩展模板参数包,为 print 生成函数形参列表。第二个扩展出现在 print 的调用中。该模式将为 print 调用生成实参列表。

Args 的扩展将模式 const Args& 应用于模板参数包 Args 中的每个元素。该模式的扩展是一个以逗号分隔的零个或多个形参类型的列表,其中每个类型的形式如 const type&。例如,

print(cout, i, s, 42);  // two parameters in the pack

最后两个实参的类型与模式一起确定了尾置形参的类型。上面的调用实例化为

ostream& print(ostream&, const int&, const string&, const int&); 

第二个扩展发生在 print 的(递归)调用中。在本例中,该模式是函数参数包的名字,即 rest。该模式扩展成一个以逗号分隔的包中元素的列表。因此,该调用等价于

print(os, s, 42);

理解包扩展

print 的函数参数包的扩展仅将包扩展为其组成部分。当扩展函数参数包时,更复杂的模式也是可能的。

// call debug_rep on each argument in the call to print
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest) {
	// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
	return print(os, debug_rep(rest)...);
} 

print 调用使用模式 debug_rep(rest)。此调用表示我们想要在函数参数包 rest 中的每个元素上调用 debug_rep()。生成的扩展包是一个以逗号分隔的 debug_rep 调用列表。

例如,调用

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

执行时,如同代码:

print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep("otherData"), debug_rep(item));

相反,下面模式会编译失败:

// passes the pack to debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // error: no matching function to call

这个调用执行时如同代码:

print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item));

注:扩展中的模式会独立地应用于包中的每个元素。

转发参数包

在C++11标准下,可以结合使用可变参数模板与 forward 来编写函数,将其实参不变地传递给其他函数。
标准库容器的 emplace_back 成员是可变参数成员模板,使用它的实参在容器管理的空间中直接构造元素。

例,在 StrVec 类中增加 emplace_back 成员。
此版本的 emplace_back 必须是可变参数的,因为 string 具有一些形参不同的构造函数。
因为想要能够使用 string 移动构造函数,所以需要保留传递给 emplace_back 的实参的类型信息。

保留类型信息有两步处理。
第一,将 emplace_back 函数形参定义成模板类型形参的右应用。

class StrVec {
public:
	template <class... Args> void emplace_back(Args&&...);
	// remaining members as in § 13.5
};

模板参数包中的扩展模式是 &&,意味着每个函数形参是其对应实参的右值引用。

第二,当 emplace_back 传递实参给 construct 时,必须使用 forward 保留实参的原始类型。

template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {
	chk_n_alloc(); // reallocates the StrVec if necessary
	alloc.construct(first_free++, std::forward<Args>(args)...);
}

construct 调用中的扩展:

std::forward<Args>(args)...

同时扩展了模板参数包 Args 和函数参数包 args。这个模式生成元素形如:

std::forward<T_i>(t_i)

其中 T_i 表示模板参数包中第 i 个元素的类型,t_i 表示函数参数包中第 i 个元素。

//  assuming svec is a StrVec
svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element

则 construct 调用的模式扩展为

std::forward<int>(10), std::forward<char>(c)

16.5 模板特例化

并非总是能够编写一个模板,适合每个能够实例化的模板实参。当我们不能(或不想)使用模板版本,可以定义类模板或函数模板的一个特例版本。

例如,重载 compare 函数处理字符串字面常量:

// first version; can compare any two types
template <typename T> int compare(const T&, const T&);
// second version to handle string literals
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);

但是,对于上面第二个版本的 compare,只有当传递字符串字面常量或字符数组时,才会调用。如果使用字符指针调用 compare,实际调用的是模板的第一个版本。

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);      // calls the first template
compare("hi", "mom"); // calls the template with two nontype parameters

为了处理字符指针(不是数组),可以定义 compare 第一个版本的模板特例化 (template specialization)。一个特例化是模板的一个独立的定义,其中的一个或多个形参被指定为特定类型。

定义函数模板特例化

当特例化一个函数模板时,必须为原始模板中的每个形参类型都提供实参。
使用关键字 template,并在其后加上一对空的尖括号,表示特例化一个模板。空尖括号表示为原始模板的所有模板形参提供实参。

// special version of compare to handle pointers to character arrays
template <>
int compare(const char* const &p1, const char* const &p2) {
	return strcmp(p1, p2);
}

当定义一个特例化版本时,函数形参类型必须匹配先前声明的模板的对应类型。
现在特例化:

template <typename T> int compare(const T&, const T&);

其中函数形参是指向 const 类型的引用。

现想要定义该函数的一个特例化,其中 T 为 const char*。一个指针类型的 const 版本是一个常量指针,而不是指向 const 的指针。因此,在特例化版本中需要使用的类型是 const char* const &,这是一个指向 const 指针的引用,该指针指向 const char。

函数重载 VS 模板特例化

特例化实际上是模板的一个实例;而不是函数名的重载版本。因此,特例化不会影响函数匹配。

例如,对于调用

compare("hi", "mom")

即使为 const T& 版本提供了接受字符指针的特例化版本,调用选择的依然是形参为字符数组的函数。

如果将接受字符指针的函数定义成普通的非模板函数,而不是特例化版本,那么调用选择的就是这个非模板函数了。

关键概念:普通作用域规则应用于特例化

为了特例化模板,原模板的声明必须在作用域中。在使用模板实例化的任何代码之前,特例化的声明必须在作用域中。

⭐实践:模板与它们的特例化版本应该声明在同一个头文件中。给定名字的所有版本的声明应该出现在前面,然后是这些模板的特例化版本。

类模板特例化

除了可以特例化函数模板,还可以特例化类模板。

例,定义标准库 hash 模板的一个特例化版本,可以用来在无序容器中存储 Sales_data 对象。默认情况下,无序容器使用 hash<key_type> 组织它们的元素。

一个特例化 hash 类必须定义:

  • 一个重载调用运算符,接受一个容器关键字类型的对象,返回一个 size_t
  • 两个类型成员,result_type 和 argument_type,分别是调用运算符的返回类型与实参类型
  • 默认构造函数和复制赋值运算符(可以隐式地定义)

复杂之处在于,当特例化模板时,必须在定义原始模板的同一命名空间中进行。
为了达成此目的,首先必须打开命名空间:

// open the std namespace so we can specialize std::hash
namespace std {
}  // close the std namespace; note: no semicolon after the close curly

出现在打开和关闭花括号之间的任何定义将成为 std 命名空间的一部分。

下面为 Sales_data 定义 hash 的特例化版本:

// open the std namespace so we can specialize std::hash
namespace std {
	template <>           // we're defining a specialization with
	struct hash<Sales_data> // the template parameter of Sales_data
	{
		// the type used to hash an unordered container must define these types
		typedef size_t result_type;
		typedef Sales_data argument_type; // by default, this type needs ==
		size_t operator()(const Sales_data& s) const;
		// our class uses synthesized copy control and default constructor
	};
	size_t
	hash<Sales_data>::operator()(const Sales_data& s) const {
		return hash<string>()(s.bookNo) ^ 
			   hash<unsigned>()(s.units_sold) ^ 
			   hash<double>()(s.revenue);
	}
} // close the std namespace; note: no semicolon after the close curly

注意,上面定义的 hash 函数计算 3 个所有数据成员的哈希值,这样 hash 函数与 Sales_data 的 operator== 定义兼容。默认情况下,无序容器使用与 key_type 对应的 hash 的特例化版本以及关键字类型上的相等运算符。

假定特例化版本在作用域中,当使用 Sales_data 作为容器的关键字时,会自动使用特例化版本:

// uses hash<Sales_data> and Sales_data operator==from § 14.3.1
unordered_multiset<Sales_data> SDset;

因为 hash<Sales_data> 使用 Sales_data 的私有成员,所以必须令其成为 Sales_data 的友元:

template <class T> class std::hash;  // needed for the friend declaration
class Sales_data {
	friend class std::hash<Sales_data>;
	// other members as before
};

注:为了使 Sales_data 的用户能够使用 hash 的特例化版本,应该将这个特例化版本定义在 Sales_data 头文件中。

类模板部分特例化

与函数模板不同,类模板的特例化不需要为每个模板形参都提供实参。可以指定一部分而非所有模板形参,或形参的一些而非所有特性。类模板部分特例化 (partial specialization) 本身是一个模板。用户必须为特例化中未指定的模板形参提供实参。

标准库 remove_reference 模板是通过一系列特例化来完成其功能的:

// original, most general template
template <class T> struct remove_reference {
	typedef T type;
};
// partial specializations that will be used for lvalue and rvalue references
template <class T> struct remove_reference<T&>  // lvalue references
{
	typedef T type;
};
template <class T> struct remove_reference<T&&> // rvalue references
{
	typedef T type;
};

部分特例化的模板形参列表是原始模板形参列表的子集或特例化。在本例中,特例化的形参个数与原模板相同。但是,特例化中的形参类型与原模板不同。

int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype(i) is int&, uses first (T&) partial specialization
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;

三个变量 a、b、c 都是 int 类型。

特例化成员而不是类

可以只特例化特定的成员函数,而不是特例化整个模板。

template <typename T> struct Foo {
	Foo(const T &t = T()): mem(t) { }
	void Bar() { /* ... */ }
	T mem;    // other members of Foo 
};
template<>           // we're specializing a template
void Foo<int>::Bar() // we're specializing the Bar member of Foo<int>
{
	// do whatever specialized processing that applies to ints
}

上面只特例化 Foo<int> 类的一个成员。Foo<int> 的其他成员由 Foo 模板提供。

Foo<string> fs;  // instantiates Foo<string>::Foo()
fs.Bar();        // instantiates Foo<string>::Bar()
Foo<int> fi;     // instantiates Foo<int>::Foo()
fi.Bar();        // uses our specialization of Foo<int>::Bar()

【C++ Primer】目录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值