学完Efficient c++ (46-47)

时间:2024-03-19 17:16:17

条款 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 的步骤如下:

  1. 确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
  2. 为该类型选择一个名称。例如 iterator_category。
  3. 提供一个模板和一组特化版本,内含你希望支持的类型相关信息。例如 iterator_traits。

以迭代器为例,标准库中拥有多种不同的迭代器种类,它们各自拥有不同的功用和限制:

  1. input_iterator:单向输入迭代器,只能向前移动,一次一步,客户只可读取它所指的东西,而且只能读取一次。(istream_iterators)
  2. output_iterator:单向输出迭代器,只能向前移动,一次一步,客户只可写入它所指的东西,而且只能涂写一次。(ostream_iterators)
  3. forward_iterator:单向访问迭代器,只能向前移动,一次一步,读写均允许。
  4. bidirectional_iterator:双向访问迭代器,去除了只能向前移动的限制。(list、set、multiset、map、multimap)
  5. 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:

  1. 建立一组重载函数或函数模板,彼此间的差异只在于各自的traits参数。令每个函数的实现与其接收的traits信息相对应。
  2. 建立一个控制函数或函数模板,它调用上述的那些函数并传递traits class所提供的信息。

iterator_traits(iterator_category、value_type 见条款42、char_traits用来保存字符类型的相关信息,numeric_limits用来保存数值类型的相关信息)