条款 46:需要类型转换时请为模板定义非成员函数
该条款与条款 24 一脉相承,还是使用原先的例子:
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T& Numerator() const;
const T& Denominator() const;
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
return Rational<T>(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 无法通过编译!
上述失败启示我们:模板实参在推导过程中,从不将隐式类型转换纳入考虑。虽然以oneHalf
推导出Rational<int>
类型是可行的,但是试图将int
类型隐式转换为Rational<T>
是绝对会失败的(模板实参推导中并不考虑采纳通过构造函数而发生的隐式类型转换)。
由于模板类并不依赖模板实参推导,所以编译器总能够在Rational<T>
具现化时得知T
,因此我们可以使用友元声明式在模板类内指涉特定函数:
template<typename T>
class Rational {
public:
...
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
...
};
在模板类内,模板名称可被用来作为“模板及其参数”的简略表达形式,因此下面的写法也是一样的:
template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
...
};
当对象oneHalf
被声明为一个Rational<int>
时,Rational<int>
类于是被具现化出来,而作为过程的一部分,友元函数operator*
也就被自动声明出来,其为一个普通函数而非模板函数,因此在接受参数时可以正常执行隐式转换。
为了使程序能正常链接,我们需要为其提供对应的定义式,最简单有效的方法就是直接合并至声明式处:
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}
由于定义在类内的函数都会暗自成为内联函数,为了降低内联带来的冲击,可以使operator*
调用类外的辅助模板函数:
template<typename T> class Rational;
template<typename T>
const Rational<T> DoMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {
return Rational<T>(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}
template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return DoMultiply(lhs, rhs);
}
...
};
条款 47:请使用 traits classes 表现类型信息
traits classes 可以使我们在编译期就能获取某些类型信息,它被广泛运用于 C++ 标准库中。traits 并不是 C++ 关键字或一个预先定义好的构件:它们是一种技术,也是 C++ 程序员所共同遵守的协议,并要求对用户自定义类型和内置类型表现得一样好。
设计并实现一个 trait class 的步骤如下:
- 确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
- 为该类型选择一个名称。例如 iterator_category。
- 提供一个模板和一组特化版本,内含你希望支持的类型相关信息。例如 iterator_traits。
以迭代器为例,标准库中拥有多种不同的迭代器种类,它们各自拥有不同的功用和限制:
-
input_iterator
:单向输入迭代器,只能向前移动,一次一步,客户只可读取它所指的东西,而且只能读取一次。(istream_iterators) -
output_iterator
:单向输出迭代器,只能向前移动,一次一步,客户只可写入它所指的东西,而且只能涂写一次。(ostream_iterators) -
forward_iterator
:单向访问迭代器,只能向前移动,一次一步,读写均允许。 -
bidirectional_iterator
:双向访问迭代器,去除了只能向前移动的限制。(list、set、multiset、map、multimap) -
random_access_iterator
:随机访问迭代器,没有一次一步的限制,允许随意移动,可以执行“迭代器算术”。(vector、deque、string)
标准库为这些迭代器种类提供的卷标结构体(tag struct)的继承关系如下:
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
将iterator_category
作为迭代器种类的名称,嵌入容器的迭代器中,并且确认使用适当的卷标结构体:
template< ... >
class deque {
public:
class iterator {
public:
using iterator_category = random_access_iterator;
...
}
...
}
template< ... >
class list {
public:
class iterator {
public:
using iterator_category = bidirectional_iterator;
...
}
...
}
为了做到类型的 traits 信息可以在类型自身之外获得,标准技术是把它放进一个模板及其一个或多个特化版本中。这样的模板在标准库中有若干个,其中针对迭代器的是iterator_traits
:
template<class IterT>
struct iterator_traits {
//iterator_category 其实是“IterT说它自己是什么”
using iterator_category = IterT::iterator_category;
...
};
为了支持指针迭代器,iterator_traits
特别针对指针类型提供一个偏特化版本,而指针的类型和随机访问迭代器类似,所以可以写出如下代码:
template<class IterT>
struct iterator_traits<IterT*> {
using iterator_category = random_access_iterator_tag;
...
};
当我们需要为不同的迭代器种类应用不同的代码时,traits classes 就派上用场了:
template<typename IterT, typename DisT>
void advance(IterT& iter, DisT d) {
if (typeid(std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)) {
...
}
}
但这些代码实际上是错误的,我们希望类型的判断能在编译期完成。iterator_category
是在编译期决定的,然而if
却是在运行期运作的,无法达成我们的目标。
在 C++17 之前,解决这个问题的主流做法是利用函数重载(也是原书中介绍的做法):
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, DisT d, std::random_access_iterator_tag) {
...
}
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, DisT d, std::bidirectional_iterator_tag) {
...
}
template<typename IterT, typename DisT>
void doAdvance(IterT& iter, DisT d, std::input_iterator_tag) {
if (d < 0) {
throw std::out_of_range("Negative distance"); // 单向迭代器不允许负距离
}
...
}
template<typename IterT, typename DisT>
void advance(IterT& iter, DisT d) {
doAdvance(iter, d, std::iterator_traits<IterT>::iterator_category());
}
在 C++17 之后,我们有了更简单有效的做法——使用if constexpr
:
template<typename IterT, typename DisT>
void Advance(IterT& iter, DisT d) {
if constexpr (typeid(std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)) {
...
}
}
总结如何使用一个traits class:
- 建立一组重载函数或函数模板,彼此间的差异只在于各自的traits参数。令每个函数的实现与其接收的traits信息相对应。
- 建立一个控制函数或函数模板,它调用上述的那些函数并传递traits class所提供的信息。
iterator_traits(iterator_category、value_type 见条款42、char_traits用来保存字符类型的相关信息,numeric_limits用来保存数值类型的相关信息)