• 条款25: 考虑写出一个不抛异常的swap函数


    1、内容引出

    swap()属于STL的一部分(算法),而后成为异常安全性编程的脊柱,以及用来处理自我赋值的可能性的一个常见的机制。它是如此的有用,适当的实现就显得十分重要。然而在它的实现的复杂度也比较高。这个条款就在讲swap()函数的实现问题。

    2、 widget 和widgetImpl 都是class时,如何写出高效的swap()

    STL 缺省情况下,由STL提供的swap算法完成,如下:

    namespace std {
    	template<typename T>
    	void swap(T&a, T&b) {
    		T temp(a);
    		a = b;
    		b = temp;
    	}
    }
    

    只要类型T支持copying函数(copy构造、copy赋值符号函数),就可以调用STL的缺省的swap。
    然而,标准库版本的swap却存在一个问题,它的实现进行了3次复制,对于某些实现来说,它导致的效率太低了。这些情况主要是指“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓的"pimpl"手法。以这种手法实现widget 代码如下:

    class WidgetImpl {
    private:
    	int i;
    };
    
    class Widgt {
    	Widgt(const Widgt&rhs);
    	Widgt&operator = (const Widgt&rhs) {
    		*imp = *rhs.imp;
    	}
    private:
    	WidgetImpl* imp;
    };
    

    在这种情况下,实际上我们只交换两个指针的指向便可,没有必要交换所指物。但是怎么才能告诉标准库的swap呢?
    答案:在std空间内全特化一个swap函数,然后在widget类内写一个成员函数swap,调用该全特化的swap函数。具体代码如下:

    class WidgetImpl {
    public:
    	int i;
    };
    class Widgt {
    public:
    	Widgt() = default;
    	Widgt(const Widgt&rhs);
    	Widgt&operator = (const Widgt&rhs) {
    		*imp = *rhs.imp;
    	}
    	void swap(Widgt& other) {
    		using std::swap;
    		swap(imp, other.imp);
    	}
    public:
    	WidgetImpl* imp;
     
    };
     
    namespace std {
    	template<>
    	void swap<Widgt>(Widgt&a, Widgt&b) {
    		a.swap(b);
    	}
    }
    

    这种做法的好处:

    • 提高了swap函数的效率
    • 与STL容器达成了一致性。(STL容器类也提供public swap函数,也全特化了对象的std::swap)

    3、如果widget 和widgetImpl 都是类模板,而非类的话,怎么实现?

    (1)想法一:偏特化(行不通的想法)
    template<typename T>
    class WidgetImpl{};
     
    template<typename T>
    class Widget{};
     
    namespace std {
    	template<typename T>
    	void swap<Widget<T>>(Widget<T>&a, Widget<T>&b) {
    		a.swap(b);
    	}
    }
    

    这种实现是不能通过编译的。widget内放一个swap成员函数时没问题的,但是我们无法偏特化std::swap()。

    (2)想法二:我们打算偏特化一个function 模板时,惯常的做法是为其添加一个重载版本。(行不通的想法)
    namespace std {
    	template<typename T>
    	void swap(Widget<T>&a, Widget<T>&b) {
    		a.swap(b);
    	}
    }
    

    但是c++不允许重载std中的模板函数,因为这其实是试图扩充std。C++允许在std中全特化某个模板,但是不允许添加新的模板。

    (3)想法三:在一个命名空间中定义一个swap 和 Widget WidgetImpl 等等模板, 在这个空间中的swap调用类中的成员函数。但与此同时建议提供一个std的特化Swap版本。

    为了简化起见,假设Widget的所有相关机能被置于命名空间WidgetStuff,于是:

    namespace WidgetStuff {
        ...                         //模板化的WidgetImpl等等
        template<typename T>        //和前面一样,内含swap成员函数
        class Widget { ... };
        ...
    
        template<typename T>        //non-member swap函数
        void swap(Widget<T>& a,     //这里并不属于std命名空间
                  Widget<T>& b)
        {
            a.swap(b);
        }
    }   
    

    此时调用swap,C++的名称查找法则(name lookup rules;更具体的说是所谓argument-dependent lookup或Kobeig lookup法则)将会找到WidgetStuff内的Widget专属版本,这正是我们所希望的。

    注意:

    • 虽然上面的做法对于class和classtemplate都行得通,但我们还是应该为class特化std::swap。所以,如果我们想让“class专属版”的swap在尽可能多的语境下被调用,我们就应该同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。
    • 没有上述命名空间,代码也行得通,相当于将他们放在了global中,不推荐这样做。

    4、用户角度

    从用户角度考虑,假设正在写一个function template,其内需要置换两个对象值。

    template<typename T>
    void doSomething(T& obj1, T& obj2)
    {
        ...
        swap(obj1, obj2);
        ...
    }
    
    (1)此时,调用了swap,但是调用的是哪一个一般化版本?
    • std既有的一般化版本?
    • 某个可能存在的特化版本?
    • 存在的T专属版本而且可能存在与某个命名空间内(非std内)

    我们希望的是调用T专属版本,并在该版本不存在的情况下,再去调用std内的一般化版本,那么正确的写法如下:

    template<typename T>
    void doSomething(T& obj1, T& obj2)
    {
        usint std::swap;    //令std::swap在此函数内可用
        ...
        swap(obj1, obj2);   //为T型对象调用最佳swap版本
        ...
    }
    

    一旦编译器看到了对swap的调用,它们便查找适当的swap并加以调用。C++的名称查找法则会确保将找到global作用域或者T所在的命名空间内的任何T专属的swap。

    (2)C++的名称查找法则的具体做法:
    • 如果T是Widget并位于命名空间WidgetStuff内,编译器就会使用“实参取决的查找规则”(argument-dependentlookup)找出WidgetStuff内的swap。
    • 如果没有T专属的swap存在,编译器就会使用std内的swap——由using std::swap这条语句,使得这个选择被曝光。
    (3)注意

    如果已经针对T将std::swap进行了特化,这个特化版本也直接会被优先使用。因此,令适当的swap被调用是比较容易的。但需要小心的是:不要添加额外的修饰符,这样会影响C++挑选适当的函数:

    std::swap(obj1, obj2);      //错误的swap调用方式
    

    上面这个举动,会迫使编译器只认std内的swap,因而不再可能调用一个定义于其他地方的适当T专属版本。

    5、总结

    关于:

    • default swap
    • member swap
    • non-member swap
    • std::swap 特化版本
    • swap的调用

    做一个总结:
    首先,如果swap的缺省实现对我们的class或class template提供可接受的效率,那么我们并不需要做其他的事情。

    其次,如果swap的缺省版本效率不足(通常就是因为class或者class template使用了某种pimpl手法),则:

    • 提供一个public swap成员函数,让它高效地置换对应类型的两个对象值。这个函数绝不能抛出异常!
    • 在我们的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
    • 如果我们正在编写一个class(而非classtemplate),为我们的class特化std::swap。并令它调用我们的swap成员函数。

    最后,如果我们调用swap,请确定包含一个using声明式,以便让std::swap在我们的函数内部可以曝光可见,然后不加任何namespace修饰符,直接去调用swap。

    6、最后,还有一点关于成员版本的swap:绝对不可以抛出异常!

    (1)原因

    swap的一个最好的应用就是为了帮助class(和class template)提供强烈的异常安全性(exception-safety)保障。

    (2)适用

    当然,这一约束只施行于成员版!不可实施于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础的,在一般情况下是允许抛出异常的。
    因此,当我们写一个自定义版本的swap时,往往需要提供以下两点:

    • 高效置换对象值的办法
    • 不抛出异常

    一般而言,上面这两个特性是连在一起的,因为高效率的swap几乎总是基于对内置类型的操作(例如pimpl首发的底层指针),而内置类型上的操作绝不会抛出异常。

  • 相关阅读:
    写在vue总结之前(一)
    前端应该掌握的web基础和网络知识
    sass之为什么要使用预处理器
    ThinkPHP简单的验证码实现
    ajax接收php返回得到一堆html代码
    Bootstrap 4,“未捕获错误:Bootstrap工具提示需要Tether(http://github.hubspot.com/tether/)”
    百度AI开放平台- API实战调用
    最短路径算法—Dijkstra(迪杰斯特拉)算法分析与实现(C/C++)
    C#避免踩坑之如何添加paint事件
    php插入mysql中文数据出现乱码
  • 原文地址:https://www.cnblogs.com/lasnitch/p/12764170.html
Copyright © 2020-2023  润新知