IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Effective C++ 55个具体做法 (Meyers) 7. 模板和泛型编程 摘录 -> 正文阅读

[C++知识库]Effective C++ 55个具体做法 (Meyers) 7. 模板和泛型编程 摘录

C++ templates的最初发展动机很直接:让我们得以建立“类型安全(type-safe)的容器”,如vector、list和map。最终人们发现,C++ templates机制自身是一部完整的图灵机:它可以被用来计算任何可计算的值。于是导出了模板元编程(template metaprogramming),创造出“在C++编译器内执行并与编译完成时停止执行”的程序。

条款41: 了解隐式接口和编译器多态

面向对象编程总是以显示接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。比如:

class Widget
{
public:
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
    ...
}


void doProcessing(Widget& w)
{
    if (w.size() > 10 && w != someNastyWidget())
    {
        Widget temp(w);
        temp.normalize();
        temp.swap();
    }
}

我们可以这样说doProcessing的w:

? ? ? ? 可以在源码中找出这个接口,看看它是什么样子,所以我们称此为一个显式接口(explicit interface)。

? ? ? ? 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism)。

Templates及泛型编程的世界,与面向对象有根本上的不同。与此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interface)和编译器多态(compile-time polymorphism)移到前头了。

template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget)
    {
        T temp(w);
        temp.normalize();
        temp.swap(w);
    }
}

现在我们在看看doPrcessing内的w?

? ? ? ? w必须支持一种接口,系由template中执行与w身上的操作来决定的。本例看来w的类型T必须支持size,normalize和swap成员函数,copy构造函数,不等比较。这一组表达式(对此template而言必须是有效编译)便是T必须支持的隐式接口。

? ? ? ? 凡是涉及到w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这边是所谓编译期多态(compile-time polymorphism)。

通常显式接口由函数的签名式(也就是函数的名称、参数类型、返回类型)构成。例如

class Widget
{
public:
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
};

隐式接口就完全不同了。它并不急于函数签名式,而是由有效表达式(valid expression)组成。

template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget)
    {
        ...
    }
}

T(w的类型)的隐式接口看起来好像有这些约束:

? ? ? ? 它必须提供一个名为size的成员函数,该函数返回一个整数值;

? ? ? ? 它必须支持一个oprator!=的函数,用来比较两个T对象。

真要感谢操作符重载(overload)带来的可能性,这两个约束都不需要满足。

隐式接口仅仅是由一组有效表达式构成,表达式自身可能看起来很复杂,但它们要求的约束条件一般而言相当直接又明确。

请记住:

class和template都支持接口(interface)和多态(polymorphism)。

对class而言接口是显式的,以函数签名为中心,多态则是通过virtual函数发生在运行期。

对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译器。

条款42: 了解typename的双重意义

template <class T> 
class Widget;

template <typename T>
class Widget;

比较喜欢typename,因为它暗示参数并非一定得是个class类型。而在只接受用户自定义类型时保留旧式的class。

然而C++并不总是将class和tyepname视为等价。

template <typename C>
void print2nd(const C& container)                   // 打印容器内的第二个元素
{   
    if (container.size() >= 2)
    {
        C::const_iterator iter(container.begin());
        ++iter;
        int value = *iter;
        std::cout << value;
    }
}

template出现的名称如果依赖于某个tempplate参数,称之为从属名称(dependent names),并不依赖则成为非从属名称(independent name)。如果从属名称在class内嵌套状,称它为嵌套从属名称(nested dependent name)。iter就是一个嵌套从属名称。

嵌套从属名称有可能导致解析(parse)困难。比如:

template <typename C>
void print2nd(const C& container)                 
{
    C::const_iterator* x;
    ...
}

看起来我们声明x为一个local变量,它是个指针,指向C::const_iterator。但如果C::const_iterator不是个类型?如果C有个static成员变量而碰巧命名为const_iterator,或x碰巧是个global变量名称?那么上述代码是一个相乘动作。

C++有个规则可以解析此一歧义状态:如果解析器知道template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。我们必须告诉C++说C::const_iterator是个类型。只要紧邻它之前放置关键字typename即可。

template <typename C>
void print2nd(const C& container)                 
{
    typename C::const_iterator* x;
    ...
}

一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它之前一个位置放上关键字typename。

typename只被用来验明嵌套从属类型名称:其他名称不该有它的存在。例如下面的function template,接受一个容器和一个指向该容器的迭代器;

template<typename C>                // 允许使用typename或class
void f(const C& container,          // 不允许使用typename
        typename C::iterator iter);  // 一定使用typename

typename必须作为嵌套从属类型名称的前缀词这一规则的例外是typename不可以出现在base class list内的嵌套从属类型名称之前,也不可以出现在member initialization list中作为base class修饰符。

template <typename T>
class Derived : public Base<T>::Nested  // base class list中不允许typename
{
public:
    explicit Derived(int x) : Base<T>::Nested(x)    // mem.init.list 不允许typename
    {
        typename Base<T>::Nested temp;              // 嵌套从属类型名称
        ...
    }
    ...
}

看一下最后一个typename例子;

template <typename IterT>
void workWithIterator(IterT iter)
{
    typename std::iterator_traits<IterT>::value_type temp(*iter);
    ...
}

value_type被嵌套在iterator_traits<IterT>之内而IterT是个template参数,所以我们必须在它之前放置typename。

如果认为多打几次这个字实在很恐怖,那么你应该会想到建立一个typedef。

template <typename IterT>
void workWithIterator(IterT iter)
{
   typedef typename std::iterator_traits<IterT>::value_type value_type;
   value_type temp(*iter);
    ...
}

请记住;

声明template参数时,前缀关键字class和typename可互换。

请使用关键字typename标识嵌套从属类型名称;但不得在base class list或member initialization list内以它作为base class修饰符。

条款43: 学习处理模板化基类内的名称

撰写一个程序,它能够传达信息到若干个不同的公司去。

class CompanyA
{
public:
    ...
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
    ...
};

class CompanyB
{
public:
    ...
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
    ...
};

...
class MsgInfo();

template<typename Company>
class MsgSender
{
public:
    ...
    void sendClear(const MsgInfo& info)
    {
        std::string msg;
        // 在这儿,根据info产生信息;
        Company c;
        c.sendCleartext(msg);
    }
    void sendSecret(const MsgInfo& info) // 类似sendClear,唯一不同是这里调用c.sendEncrypted
    {...}

};

假设我们有时候想要在每次送出信息时log某些信息。drived class可轻易加上这样的生产力,那么;

template <typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        // 将传送前的信息写至log;
        sendClear(info);
        // 将传送后的信息写至log
    }
    ...
};

问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的MsgSender<Company>,但其中的Compant是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么--更明确地说没办法知道它是否有个sendClear函数。

为了让问题更具体化,假设我们有个class CompanyZ坚持使用加密通讯:

class CompanyZ      // 这个class不提供sendCleartext函数
{
public:
    ...
    void sendEncrypted(const std::string& msg);
    ...    
};

template<>                  // 一个全特化的MsgSender,它和一般template相同,
class MsgSender<CompanyZ>   // 差别在于它删除掉了sendClear。
{
public:
    ...
    void sendSecret(const MsgInfo& info){...};
    ...
};

注意cass定义式最前头的“template<>”语法象征这既不是template也不是标准class,而是个特化版本的MsgSender template,在template实参是CompanyZ时被使用。这是所谓模板全特化(total template specialization):template MsgSender针对类型CompanyZ特化了,而且其特化时全面性的,也就是说一旦类型参数被定义为CompanyZ,在没有其他template参数可供变化。

template <typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        // 将传送前的信息写至log;
        sendClear(info);            // 如果Company=CompanyZ,这个函数不存在
        // 将传送后的信息写至log
    }

};

因为C++知道base class template有可能被特化,而那个特化版本可能不提供和一般性template相同的借口。C++往往拒绝在template base class内寻找继承而来的名字。然而就某种意义而言,当我们从Objected Oriented C++ 跨进Template C++,继承就不像以前那般畅行无阻了。

我们必须有某种办法令C++不进入templatized base classes观察的行为失效。有三个办法,第一是在base class函数调用之前加上“this->”:第二是使用using声明式。并不是base class名称被derived class名称遮掩,而是编译器不仅如此base class作用域内查找,于是我们通过using告诉它,请它这样做。第三个做法是,明白指出被调用的函数位于base class内,但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰会关闭virtual绑定行为。

从名称可视点的角度出发,上述每一种解法做的事情都相同:对编译器承诺base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口。

template <typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    void sendClearMsg(const MsgInfo& info)
    {
        // 将传送前的信息写至log;
        this->sendClear(info);            // 成立,假设sendClear将被继承
        // 将传送后的信息写至log
    }

};

template <typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    using MsgSender<Company>::sendClear;
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        ...
        sendClear(info);            // 成立,假设sendClear将被继承
        ...
    }

};

template <typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
        ...
       MsgSender<Company>::sendClear(info);            // 成立,假设sendClear将被继承
        ...
    }
};

本条款探讨的是,面对“指涉base class member”之无效reference,编译器的诊断时间可能发生在早期(当解析derived class template的定义式时),也可能发生在晚期(当那些template被特定值template实参具现化时)。C++的政策是宁愿早诊断,这就是为什么“当base class从template中被具现化时”它假设它对那些base class的内容毫无所悉的缘故。

请记住;

可在derived class template内通过this->指涉base class template内的成员名称,或藉由一个明白写出的base class资格修饰符完成。

条款44: 将与参数无关的代码抽离templates

template是节省时间和避免代码重复的一个好方法。不过使用template可能会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据、或二者。

代码template带来的代码膨胀主要工具为:共性和变性分析(commonality and variability analysis)。

当两个函数有些代码重复,我们会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。

在编写template时,重复是隐晦的:毕竟只有只存在一份template源码,所以你必须训练自己去感受template被具现化多次时可能发生的重复。

比如,为固定尺寸的正方形矩阵编写一个template

template <typename T, std::size_t n>
class SquareMatrix
{
public:
    ...
    void invert();    
};

// 考虑以下代码
SquareMatrix<double, 5> sm1;
...
sm1.invert();
SquareMatrix<double, 10> sm2;
...
sm2.invert();

这会具现两份invert。

如果我们看见两个函数完全相同,数值不同。我们会建立一个带数值参数的函数。

template <typename T>
class SquareMatrixBase
{
protected:
    ...
    void invert(std::size_t matrixSize);
    ...    
};

template <tyepname T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
    using SquareMatrixBase::invert;
public:
    ...
    void invert() { this->invert(n); }    
};

但还有一个棘手的问题没有解决,SquareMatrixBase::invert如何知道该操作什么数据?虽然它每从参数中知道矩阵尺寸,但它知道那个特定矩阵的数据在哪儿?想必只有derived class知道。Derived class是如何联络其base class做逆运算动作? 一个可能的做法为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。这似乎不方便!

另一个办法是令SquareMatrixBase储存一个指针,指向矩阵数值所在的内存,而只要存储了那些东西,也就可能知道矩阵尺寸。

template <typename T>
class SquareMatrixBase
{
protected:
    ...
    SquareMatrixBase(std::size_t n, T* pMem) 
    : size(n), pData(pMem) {}                   // 存储矩阵大小和一个
                                                // 指针,指向矩阵数值。
                                                // 重新赋值给pData
    void setDataPtr(T* ptr) { pData = ptr; }
    ...

private:
    std::size_t size;
    T* pData;    
};

template <tyepname T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
    T* data[n*n];
    
public:
    SquareMatrix() : SquareMatrixBase<T>(n, data){} // 送出矩阵大小和数据指针给
    ...                                             // base class

};

这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是把每一个矩阵的数据放进heap。

template <tyepname T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T>
{
private:
    boost::scoped_array<T> pData;
    
public:
    SquareMatrix() 
    : SquareMatrixBase<T>(n, 0),        // 将base class的数据指针设为null
    pData(new T[n*n])                   // 为矩阵内容分配内存,将指向该内存的指针存储起来
    {  this->setDataPtr(pData.get()); } // 然后将它的一个副本交给base class
    ...                                            

};

代价就是硬绑着矩阵尺寸的那个invert版本,有可能生成比共享版本(其中尺寸仍然以函数参数传递或存储在对象内)更佳的代码。

从另一个角度看,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就是降低程序的working set(指一个在虚拟环境下执行的进程而言,其所使用的那一组内存页)。

你越是尝试精密做法,事情变得越是复杂。

本条款只讨论由non-type parameters带来的膨胀,其实type parameter也会导致膨胀。

请记住;

Template生成的多个class或函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量代替template参数。

因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representations)的具现类型(instantiation types)共享实现码。

条款45: 运用成员函数模板接受所有兼容类型

真实指针做的很好的一件事情是,支持隐式转换(implicit conversion)。Derived class指针可以隐式转换为base class指针,“指向non-const对象”的指针可以转换为“指向const对象”

class Top {...};
class Middle : public Top {...};
class Bottom : public Middle {...};

Top* pt1 = new Middle;      // 将Middle* 转换为Top*
Top* pt2 = new Bottom;      // 将Bottom* 转换为Top*
const Top* pct2 = pt1;      // 将Top* 转换为const Top*

SmartPtr<Top> pt1 = SmartPtr<Middle> (new Middle); 
SmartPtr<Top> pt1 = SmartPtr<Bottom> (new Bottom);

但是,同一template的不同具现体之间并不存在什么与生俱来的固有关系。

一个很重要的观察结果是:我们永远无法写出我们需要的所有构造函数。似乎我们需要的不是为SmartPtr写一个构造函数,而是为它写一个构造模板。这样的模板(template)是所谓的member function template,其作用就是为class生成函数。

template <typename T>
class SmartPtr
{
public:
    template <tyepname U>
    SmartPtr(const SmartPtr<U>& other);
    ...    
};

这一类构造函数根据对象U创建对象T,而U和V是同一个template的不同具现体,有时我们成为泛化generalized copy构造函数。

上述的泛化copy构造函数并未被声明为explicit。那是蓄意的,因为原始指针类型之间的类型转换(例如derived class指针转换为base class指针)是隐式转换,无需显式写出转型动作cast,所以让智能指针仿效这种行径也属合理。但是现实中并没有将int*转化为double*的对于隐式转换行为,所以我们必须对某方面的这一member template多创建的成员函数进行挑选或筛除。

template <typename T>
class SmartPtr
{
public:
    template <tyepname U>
    SmartPtr(const SmartPtr<U>& other)
    : heldPtr(other.get()) {}           // 以other的heldPtr初始化this的heldPtr

    T* get() const { return helder; }
    ...    
private:
    T* heldPtr;
};

这个行为只有当“存在某个隐式转换可将一个U指针转换为T指针”时才能通过编译,而那正是我们想要的。

member function template的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。

在class内声明一个泛化copy构造函数,并不会阻止编译器生成它们自己的copy构造函数,所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和正常的copy构造函数。相同规则也适用于赋值操作。

请记住;

请使用member function template生成可接受所有兼容类型的函数;

如果你声明member template用于泛化copy或泛化赋值操作,你还是需要声明正常的copy构造函数和copy赋值操作。

条款46: 需要类型转换时请为模板定义非成员函数

template <typename T>
class Rational
{
public:
    Rational(const T& numerator = 0, const T& dominator = 1);
    const T numerator() const;
    const T denominator() const;
    ...
};

template <typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{...}

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; //错误,无法通过编译

此时编译器不知道我们想要调用哪个函数,它们试图想出什么函数被名为operator*的template具现化出来,它们知道它们应该可以具现化某个名为operator*并接受两个Rational<T>参数的函数,但为完成这一具现化行动,必须先算出T是什么,问题是它们没有这个能耐。因为template实参推导过程从不将隐式类型转换函数纳入考虑。绝不!这样的转换在函数调用过程的确被使用,但在能够调用一个函数之前,首先必须知道那个函数存在。而为了知道它,必须先为相关的function template推导出参数类型(然后才能将适当的函数具现化出来)。

我们需要利用一个事实,就可以缓和编译器在template实参推导方面受到的挑战:template class内friend声明式科研指涉某个特定函数。class template并不依赖template实参推导(后者只施行于function template身上),所以编译器总是能够在class Rational<T>具现化时知道T。

template <typename T>
class Rational
{
public:
    ...
    friend  const Rational operator* (const Rational& lhs, const Rational& rhs);
};

template <typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
{...}

现在operator*的混合式调用就可以通过编译了,因为当对象oneHalf被声明为一个Rational<T>,class Rational<int>于是被局限出来,而作为过程的一部分,friend函数operator(接受Rational<T>)也被自动声明出来。后者身为一个函数而非函数模板(function template),因此编译器可在它调用时使用隐式转换函数(例如Rational的non-explicit构造函数),这便是混合式调用之所以成功的原因。但是却通不过链接。

在一个class template内,template名称可被用来作为template和其参数的简略表达式,所以再Rational<T>内我们只写Rational而不必写Rational<T>。

目前编译器知道我们要调用那个函数(就是接受一个Rational<T>以及又一个Rational<T>),但是函数只被声明在Rational内,并没有被定义出来。我们意图令此class外部的operator* template提供定义式,但是行不通---如果我们自己声明了一个函数,就有责任定义那个函数,既然我们没有提供定义式,连接器当然找不到它。

或许最简单可行的办法就是将operator*函数本体合并在其声明式内。万岁,目前可以运行起来了。

template <typename T>
class Rational
{
public:
    ...
    friend  const Rational operator* (const Rational& lhs, const Rational& rhs)
    {
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator )
    }
};

但是这个技术的趣味点在于,我们使用了friend,却与friend的传统用途“访问class的non-public成分”毫不相干。为了让类型转换发生于所有实参身上,我们需要一个non-member函数,为了令这个函数被自动具现化,我们需要将其声明在class内部。

另一种做法是,定义于class内的函数都暗自成为inline函数,包括friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是零operator*不做任何事情,只调用一个定义于class外部的辅助函数。对于更复杂的函数而言,这样做也许就有价值。

template <typename T>
const Rational<T> doMultiply();

template <typename T>
class Rational
{
public:
    ...
    friend  const Rational operator* (const Rational& lhs, const Rational& rhs)
    {
        return doMultiply(lhs, rhs);
    }
};

template <typename T>
const Rational<T> doMultiply(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator )
}

许多编译器会强迫你讲所有template定义式放在头文件中,所以你或许需要在头文件内定义doMultiply函数。

请记住:

当我们编写一个class template,而它所提供值“与此template相关的”函数支持“所有参数值隐式类型转换时”,请将这些函数定义为class template内部的friend函数。

条款47: 请使用traits classes表现类型信息

STL共有5种迭代器分类,对应于它们支持的操作。Input迭代器只能向前移动,一次一步,客户只读取(不能修改)它们所指的东西,而且只能读取一次。

outPut迭代器类似,它们只能向前移动,一次一步,客户只可涂写它们所指东西,而且只能涂写一次。

另一个比较强大的分类是forward迭代器。这种迭代器可以做到前述两种分类做的每一件事,而且可以读写其所指物一次以上。比如单向链表,TR1 hased容器。

Bidirectional迭代器更强,它除了可以向前移动,还可以向后移动。STL的list迭代器就属于这一分类。set,map和unorder版本即是。

最强的当属random access迭代器,因为他可以执行“迭代器算术”,也就是它可以在常量时间内向前或向后跳跃任意距离。内置指针即为。vector,deque,string提供的迭代器也是。

对于这5种分类,C++标准程序库分别提供专属的卷标结构(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 {};

现在回到advance函数,

template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    if (iter is a random access iterator) // 针对random access迭代器采取迭代器算术运算 
    {
        iter += d;
    }
    else
    {
        if(d >= 0) { while(d--) ++iter; }
        else{ while(d++) --iter; }
    }
}

此时需要判断iter是否为random access迭代器。我们可以用trait在编译期间取得某些信息。

Traits并不是C++关键字预先定义好的构件:它们是一个技术,也是一个C++程序员共同遵守的协议。其要求之一是,它对内置类型和用户自定义类型的表现一样好。

traits必须能施行于内置类型,意味着类型内的嵌套信息这种东西出局了,因为我们无法将信息嵌套与原始指针内。标准技术是把它放入一个template及一个或多个特化版本。

template <typename IterT>
struct iterator_traits;

iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。

Iterator_traits以两个部分实现上述所言。首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构。一个针对deque、list迭代器而设计的class看起来是:

template <...>
class deque
{
public:
    class iterator
    {
    public:
        typedef random_access_iterator_tag iterator_category;
        ...
    };
    ...
};

template <...>
class deque
{
public:
    class iterator
    {
    public:
        typedef bidirectional_iterator_tag iterator_category;
        ...
    };
    ...
};

// 至于iterator_triats
template <typename IterT>
struct iterator_traits
{
    typedef typename IterT::iterator_category iterator_category;
    ...
};

iterator_traits的第二个部分如下,专门用来对付指针。为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本(partial template specification)。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:

template <typename IterT>
struct iterator_traits<IterT*>
{
    typedef random_access_iterator_tag iterator_category;
};

如何设计并实现一个traits class:

? ? ? ? 确认若干你希望将来取得的类型相关信息;

? ? ? ? 为该信息选择一个名称,例如iterator_category;

? ? ? ? 提供一个template和一组特化版本,内含你希望支持的类型相关信息。

有了iterator_traits,我们可以对之前advance实现伪代码;

template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    if (typeid(typename std::iterator_traits<IterT>::iterator_category == typeid(std::random_access_iterator_tag))) 
    ...
}

template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
    iter += d;
}

template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
    iter += d;
}


template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category);
}

虽然它会导致编译问题。等会处理。为什么将可在编译器完成的事延到运行期才做呢? 这不仅浪费时间,也造成可执行文件膨胀。如果我们真的想要一个条件语句(if之类),我们可以用函数重载,其中一个参数为迭代器类型。

现在我们可以总结如何使用一个traits class了;

? ? ? ? 建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此之间的差异在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。

? ? ? ? 建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述那些劳工函数,并传递traits class所提供的信息。

请记住:

Traits class使得“类型相关信息”在编译器可用。它们以template和template 特化完成实现;

整合重载技术后,traits class有可能在编译器对类型执行if..else测试。

条款48: 认识template元编程

Template metaprogramming(TMP,模板元编程)是编写template based C++程序并执行于编译器的过程。

TMP有两个伟大效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至是不可能的。第二,由于template metaprogramming执行于C++编译器,因此可将工作从运行期转移到编译器。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译器找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转移到编译器的另一个结果是:编译时间变长了。是的,程序如果使用TMP,其编译时间可能远长与不使用TMP的对应版本。

template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) 
    {
        iter += d;
    }
    else
    {
        if(d >= 0) { while(d--) ++iter; }
        else{ while(d++) --iter; }
    }
}

条款47指出,这个typeid-based解法的效率比traits解法低,因为在此方案中,

????????(1)类型检测发生在运行期而非编译期;

? ? ? ? (2)运行期类型测试代码会出现在(或说被链接于)可执行文件中;

我们也在条款47说出,advance的typeid-based实现方式可能导致编译期问题,如下:

std::list<int>::iterator iter;
...
advance(iter, 10);  // 无法通过编译

因为list的迭代器是个bidirection迭代器,不支持随机读取。尽管编测试typeid的哪一行总是会因为list<int>::iterators而失败,但编译器必须确保所有源码都有效,纵使是不会执行的代码!而当iter不是random_access时,iter+=d无效。与此对比的是traits-based TMP解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可实行于该函数所对付的类型。

TMP已被证明是个图灵完全(Turing-complete)机器,意思是它的威力达到足以计算任何事物。使用TMP你可以声明变量、执行训话、编写及调用函数...

例如TMP如何实现循环。TMP并没有真正的循环构建,所以循环效果是有递归完成的。TMP主要是个函数式语言,而递归之余=于这类语言很重要。TMP的递归甚至是不同正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。例如阶乘的实现:

template <unsigned n>
struct Fractorial
{
    enum {value = n * Fractorial<n-1>::value};
};

template <>
struct Fractorial<0>
{
    enum {value = 1};
};

循环发生在Template具现体Fractorial<n>内部指涉另一个template具现体Factorial<n-1>之时。和所有良好的递归一样,我们需要一个特殊情况造成递归结果。这里的特殊情况就是template特化体Fractorial<0>。

为领悟TMP,很重要一点是先对它能够达成什么目标有一个比较好的理解,下面举3个例子:

? ? ? ? 确保量度单位正确;

? ? ? ? 优化矩阵运算;

? ? ? ? 可以生成客户定制的设计模式;

由于TMP是一个在相对较短时间之前才意外发现的语言,其编程方式还多少依赖经验。尽管如此,将工作从运行期移往至编译期所带来的效率改善还是令人印象深,而TMP对“难以或甚至不可能于运行期实现出来的行为”的表现能力也很吸引人。

请记住:

Template metaprogramming可将工作由运行期移至编译期,因而得以实现早期错误侦查和更高的执行效率。

TMP可被用来生成“基于政策选择组合(based on combiations of policy choice)”客户定制代码

,也可以避免生成对某些特殊类型并不适合的代码。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:11:57  更:2022-04-24 09:12:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 21:38:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码