zoukankan      html  css  js  c++  java
  • EffectiveC++ 第7章 模板与泛型编程

    我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的“可能比较准确”的「翻译」。

    Chapter 7 模版与泛型编程

    Templates and Generic Programming

    本章无法使你成为一个专家级的template程序员,但可以使你成为一个比较好的template程序员。本章也会给你必要信息,使你能够扩展你的template编程,到达你所渴望的境界。


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

    在oop的世界里,我们总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。举个例子,给定这样(没啥意义)的class:

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

    和这样的函数(也没啥意义):

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

    我们可以这么理解此函数内的 w :

    • 由于w的类型是Widget,所以w必须支持Widget接口。我们可以在例如Widget.h中的源代码找出这个接口,看看是什么样子,此时称它为显式接口,也就是它在源码中明确可见。

    • 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是会在运行期间根据w的动态类型(条款37)调用匹配的函数。

    但Template以及泛型编程的世界,与oop有根本上不同。在此世界中显式接口和运行期多态存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译器多态(compile-time polymorphism)相当重要。看看例子:

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

    现在如何理解doProcessing内的w呢?

    • w必须支持哪种接口,系由template中对w的操作而定。本例看来w的类型T似乎必须支持size、normalize和swap成员函数、copy构造函数、不等比较。后面我们会知道这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式便是T必须支持的一组隐式接口。

    • 凡设计w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用成功。具现行为发生在编译期。”以不同的template参数具现化function templates”会导致调用不同函数,这便是所谓编译期多态。

    你应该不陌生“运行期多态”和“编译期多态”之间的差异,因为它类似于“哪个重载函数该被调用(发生在编译期)”和“哪个virtual函数该被绑定(运行期)”之间的差异。而显式接口和隐式接口的差异比较新颖

    通常显式接口由函数的签名式(函数名、参数类型、返回类型)构成

    例如Widget class:

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

    它的public接口由一个构造函数、一个析构函数、函数size、normalize、swap及其参数类型、返回类型、常量性构成。当然也包括编译器产生的拷贝构造函数和拷贝赋值(copy assignment)操作符。另外也可包括typedefs,甚至是你强制声明的public成员变量

    隐式接口则并不基于函数签名式,而是由有效表达式(valid expressions)组成。

    再看看doProcessing template一开始的条件:

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

    T(w的类型)的隐式接口看来似乎有这些约束:

    • 它必须提供一个名为size的成员函数,此函数返回一个int
    • 它必须支持一个 operator!= 函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T

    由于operator overloading带来的可能性,这两个约束都不需要满足。T必须支持size函数,但此函数可能从base class继承而来,这个成员函数不需返回一个int、甚至不需返回数值。由此来看,它甚至不需返回一个定义有operator>的类对象或其引用!它唯一需要做的是返回一个类型为X的对象,而X对象与一个int(也就是10)必须能调用一个operator>。这个operator>不需要非得取一个类型为X的参数不可,因它也可以取得类型Y的参数,只要存在一个隐式转换能将X对象转换为类型Y对象。

    同理,T不需支持operator!=,因为以下这样也是可以的:

    operator!= 接受一个类型为X的对象和一个类型为Y的对象,T可悲转换为X而someNastyWidget 的类型可被转换为Y,这样即可有效调用 operator!= 。

    当我们第一次以这种方式思考隐式接口,会觉得头疼。隐式接口仅有一组有效表达式构成,表达式自身也许看起来复杂,但它们要求的约束条件一般相当直接而明确。例如以下表达式:

    if(w.size() > 10 && w != someNastyWidget){

    if语句的条件式必须是个bool表达式,所以无论涉及什么实际类型,无论“w.size() > 10 && someNastyWidget”导致什么,它都必须与bool兼容。这是template doProcessing加诸于其类型参数T的隐式接口的一部分。doProcessing要求的其他隐式接口:拷贝构造函数、normalize和swap也都必须对T型对象有效

    请记住:

    • classes和templates都支持interfaces和多态(polymorphism)
    • 对classes而言接口是显示的,以函数签名为中心。多态则通过virtual函数发生于运行期
    • 对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则通过template具现化和函数重载解析发生于编译期

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

    提问:以下template声明式中,class和typename有何不同?

    template<class T> class Widget
    template<typename T> class Widget

    答案:完全相同。只不过某些程序员因为可少打几个字选择class,其他人喜欢typename,因为其暗示参数并非一定是一个class类型。

    然而C++并不总把class和typename视为等价。有时你一定得使用typename。为了了解这种情况,我们必须先谈谈你可以在template内指涉(refer to)的两种名称。

    假设有一个template function,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints。再假设此函数仅打印其第二元素的值。这是个无聊的函数,下面是实践的一种方式:

    template <typename C>
    void print2nd(const C& container)
    {                                   // 注意这不是有效的c++代码
        if(container.size() >= 2){
            C::const_iterator iter(container.begin());
            ++iter;  // 将迭代器移往第二个元素
            int value = *iter;
            std::cout << value;
        }
    }
    

    现在代码中强调两个local变量iter和value。iter的类型是C::const_iterator,实际怎样取决于template参数C。template内出现的名称若相依于某template参数,称之为从属名称。若从属名称在class内呈嵌套状,我们称它为嵌套从属名称。C ::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称,也就是个嵌套从属名称且指涉某类型。

    print2nd内的另一个local变量value,类型为int。int是一个并不倚赖任何template参数的名称。这样的名称是谓非从属名称。

    嵌套从属名称可能会导致解析(parsing)困难。举个例子,我们将print2nd改成这样:

    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变量名称?那样的话上述代码不再是声明一个local变量,而是一个相乘动作:

    C::const_iterator乘以x

    在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型。当编译器开始解析template print2nd时,尚未确知C是什么。C++有一个规则可解析此歧义状态:若解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。所以默认情况下嵌套从属名称不是类型。此规则有个例外,稍后提到。

    现在看看print2nd的起始处:

    template <typename C>
    void print2nd(const C& container)
    {                                   
        if(container.size() >= 2){
            C::const_iterator iter(container.begin()); //这个名称被假设为非类型
    		   ...
    

    现在清楚为啥这不是有效的C++代码了吧。iter声明式只有在C::const_iterator是个类型时才合理,当我们并没有告诉C++说它是,于是C++假设它不是。解决办法是紧邻它之前放置关键字typename即可

    template<typename C>		//这是合法的C++代码
    void print2nd(const C& container)
    {
        if(container.size() >= 2){
            typename C::const_iterator iter(container.begin());
    	      ...
        }
    }
    

    一般性规则很简单:任何时候你想在template中指涉一个嵌套从属类型名称,必须在它前面放置关键字typename。(很快会谈到一个例外)

    typename仅用来验明嵌套从属类型名称;其它名称不该有它存在。例如下面这个函数模版,接受一个容器和一个“指向该容器”的迭代器:

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

    这一规则的例外是,typename不可出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。例如:

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

    让我们看看最后一个typename例子,那是你将在真实程序中看到的代表性例子。假设我们在撰写一个function template,它接受一个迭代器,而我们打算为该迭代器指涉的对象做一份local附件temp:

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

    std:: iterator_traits::value_type是标准traits class(条款47)的一种运用,相当于说“类型为IterT之对象所指之物的类型”。这个语句声明一个local变量temp,使用IterT对象所指物的相同类型,并将temp初始化为iter所指物。比如IterT是vector ::iterator,则temp的类型就是int。

    如果你觉得这个名字太长了, 便想建立一个typedef。对于traits成员名称如value_type,普通的习惯是设定typedef名称用以代表某个traits成员名称,于是常常可看到类似这样的local typedef:

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

    记得在typedef里加上必要的typename关键字!

    值得注意的是,某些编译器接受的代码原本有的typename却被遗漏了;原本不该有typename的出现了;有的旧版本编译器直接拒绝typename。这意味在移植性方面会带给你头疼。

    请记住:

    • 声明template参数时,前缀关键字class和typename可互换
    • 请使用typename标识嵌套从属类型名称;

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

    ---持续更新中

  • 相关阅读:
    团队开发冲刺第二十天
    团队开发冲刺第十九天
    第十六周进度总结
    学期课后个人总结
    用户场景分析
    第十五周进度总结
    对正在使用的输入法进行评价
    java中实现客户姓名添加和显示
    指定查找区间,查找学生姓名并显示是否修改成功
    linux下如何修改文件的权限chmod
  • 原文地址:https://www.cnblogs.com/1Kasshole/p/9394339.html
Copyright © 2011-2022 走看看