zoukankan      html  css  js  c++  java
  • 四、设计和声明--条款24-25

    条款24:若所有的参数皆需类型转换,请为此采用non-member函数

    对于能够隐式转换的,我们要得知其危险性。否则将会发生你从未考虑到的错误。用我们一直在用的分数相乘的例子来看:

    class Rational
    {
    public:
        Rational(int numberator = 0, int denominator = 1);
        int numberator();   // 获取分子
        int denominator();  // 获取分母
        const Rational operator*(const Rational &rhs) const;
    private:
        int numberator;
        int denominator;
    };
    

    仔细看这个类的构造函数:我们刻意地没有把它声明成explicit。 说明这个类是支持隐式转换的。也就是支持一个int-to-Rational的转换。

    在上段代码中我们采用的是member函数重载乘号。现在回到我们的条款上来:为什么要采用一个non-member函数呢?上述做法有什么弊端吗?

    调用情景一

    Rational oneEighth(1,8);  // 八分之一
    Rational oneHalf(1,2);    // 二分之一
    Rational result = oneHalf * oneEighth;  // 正确调用
    result = result * oneEighth;    // 正确调用
    

    使用两个对象相乘,完全正确的执行了。

    调用情景二

    result = oneHalf * 2;    // 正确
    result = 2 * oneHalf;    // 错误!!!
    

    在此场景中,第一个语句中的“2”会被隐式转换成Rational对象(因为我们并未声明为explicit函数). 故调用成功。

    那么为什么第二个语句中的“2”并没有转换成Rational对象呢?我们换个形式写上面两个语句就清晰了:

    result = oneHalf.operator(2); // 正确
    result = 2.operator(oneHalf); // 错误
    

    现在一目了然了:

    首先,oneHalf是一个对象,调用了重载函数,参数是“2”,隐式转换成一个Rational对象,所以可以正确调用。

    而在第二个语句中,数字2并不是一个对象,也就没有所谓的重载成员运算符函数,所以无法调用。

    实际上,编译器如果没有在类对象中找到相应的operator* 函数的话,还会在命名空间或全局上选择看有没有一个non-member版本的operator* :

    result = operator*(2, oneHalf);
    

    但是在我们的例子里面并不存在这一个接受int和Rational作为参数的non-member operator*,因此查找失败。

    调用情景二中为何第一个语句是如何执行的?

    隐式类型转换。 再看下我们的语句:

    result = oneHalf * 2;
    

    (1) 编译器知道我们函数需要的是一个Rational对象。

    (2) 编译器同时也知道我们传递的是一个int.

    (3) 编译器还知道只要我们将int作为Rational的构造函数参数就可以构造出一个适当的Rational对象出来。

    于是编译器就这么构造了,相当于:

    const Rational temp(2);
    result = oneHalf * temp;
    

    所以我们对第一个语句就可以成功进行调用。前提是我们并未将之声明为explicit

    所以我们采取non-member函数来进行调用:

    const Rational operator*(const Rational& lhs,const Rational &rhs)
    {
        return Rational(lhs.numberator() * rhs.numberator(), 
                       lhs.denominator() * rhs.denominator());
    }
    

    这样的话,这两个语句都会通过编译:

    result = oneHalf.operator(2); // 正确
    result = 2.operator(oneHalf); // 正确
    

    作者总结

    如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

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

    标准程序库中,我们已经有一个swap函数:

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

    因为要支持 T temp(a)做法,所以T类型需要支持copying(通过copy构造函数和copy assignment运算符)。

    有了缺省的swap,为何我们还需要自己写一个swap呢?

    下面通过一个例子介绍。在这个例子之前,我们先介绍pimpl手法。

    pimpl手法

    pointer to implementation缩写。“以指针指向一个对象,内含真正数据。” 下面用这种手法设计一个Widget Class类:

    // 这是Widget的一个实现类,即WidgetImpl
    class WidgetImpl
    {
    public:
        ...
    private:
        int a, b, c;
        std::vector<Widget> vctW;
        ...         // 假设很多个成员变量
    };
    // 这是一个处理WidgetImpl的类,处理自我赋值
    // 里面有一个指向WidgetImpl的指针
    class Widget
    {
     public:
        Widget(const Widget& rhs);
        Widget &operator=(const Widget& rhs)
        {
            WidgetImpl *pTemp = pImpl;
            pImpl = new WidgetImpl(*this.pImpl);
            delete pTemp;
            return *this;
        }
    private:
        WidgetImpl *pImpl;
    };
    

    pimpl手法在此例子的弊端:

    如果我们用swap方法实现自我赋值,我们的想法是交换两个指针即可,但是使用pimpl手法,缺省的swap算法不仅仅复制三个Widget,它还复制了三个WidgetImpl对象!!! 这极其地影响效率。

    所以我们要考虑自己实现一个不抛异常的swap函数。

    使用模板全特化,为Widget函数创建一个特定的swap函数

    先把代码写出来,随后我们根据代码来分析并简要说一下模板全特化。

    namespace std
    {
        template<>
        void swap<Widget> (Widget &a, Widget &b)
        {
            swap(...); // 调用真实交换操作的swap函数
        }
    }
    

    (1) 通常我们不允许改变std命名空间的任何东西,但可以为标准templates制造特化版本,使它专属于我们自己的class

    (2) 如代码所示:
    +
    template<>
    这一行表示模板它是下面一行std::swap函数的一个全特化版本。

    • 表示这是针对Widget类型的全特化。也就是说,当一般性的swap template施行于Widget身上的时候就会启用这个版本,其它类型会调用缺省的swap版本。

    也就是说,我们写了一个专属于Widget类的swap版本。

    现在我们考虑如果写函数里面真实的交换操作的swap函数。

    class Widget
    {
    public:
       ... // 其它函数
       void swap(Widget &other)
       {
           using std::swap;
           swap(pImpl, other.pImpl);    // 调用std内缺省的swap函数!
       }
       
    private:
        WidgetImpl *pImpl;
    };
    

    现在我们基本就把我们所要的专属swap版本写完整了。我把整个完全的简单的swap代码实现了一下:

    #include <iostream>
    #include <vector>
    using namespace std;
    // 这是Widget的一个实现类,即WidgetImpl
    class WidgetImpl
    {
    public:
    	WidgetImpl(int iA, int iB, int iC)
    		:a(iA), b(iB), c(iC)
    	{
    		
    	}
    	int a, b, c;	// 为了例子能够简单访问,不再写一个访问接口,故声明为public
    };
    // 这是一个处理WidgetImpl的类,处理自我赋值
    // 里面有一个指向WidgetImpl的指针
    class Widget
    {
    public:
    	Widget(){};	// 为了例子简单,函数体为空
    	Widget(const Widget& rhs){};	// 为了例子简单,函数体为空
    	Widget &operator=(const Widget& rhs)
    	{
    		WidgetImpl *pTemp = pImpl;
    		pImpl = new WidgetImpl(*this->pImpl);
    		delete pTemp;
    		return *this;
    	}
    	void swap(Widget &other)
    	{
    		cout << "this is a member fuction swap version" << endl;	// 成员函数swap
    		using std::swap;
    		swap(pImpl, other.pImpl);    // 调用的是std命名空间下的缺省swap函数!
    	}
    	void SetP(WidgetImpl *p)	// 设置WidgetImpl指针
    	{
    		this->pImpl = p;
    	}
    	WidgetImpl* GetP()			// 获取WidgetImpl指针
    	{
    		return this->pImpl;
    	}
    private:
    	WidgetImpl *pImpl;
    };
    
    namespace std
    {
    	template<>
    	void swap<Widget>(Widget &a, Widget &b)
    	{
    		cout << "this is total template specialization version!" << endl;	// 全特化版本
    		a.swap(b);
    	}
    }
    
    int main()
    {
    	// 定义两个WidgetImpl变量,作为Widget的成员
    	WidgetImpl wp1(1, 2, 3);
    	WidgetImpl wp2(9, 8, 7);
    	
    	// 输出原来WidgetImpl变量的值
    	cout << wp1.a << " " << wp1.b << " " << wp1.c << endl;
    	cout << wp2.a << " " << wp2.b << " " << wp2.c << endl;
    	
    	// 定义两个Widget变量
    	Widget w1, w2;
    	
    	// 将WidgetImpl设置为其成员
    	w1.SetP(&wp1);
    	w2.SetP(&wp2);
    
    	// 交换两个指针
    	swap(w1, w2);
    
    	// 输出交换之后的值
    	WidgetImpl *p1 = w1.GetP();
    	WidgetImpl *p2 = w2.GetP();
    	cout << p1->a << " " << p1->b << " " << p1->c << endl;
    	cout << p2->a << " " << p2->b << " " << p2->c << endl;
    
    	return 0;
    }
    

    输出如下:

    总结全特化版本的swap

    从我自己实现的代码来看就一目了然了:

    (1) 首先,main函数中调用的swap,就是我们全特化版本的swap,从我们控制台输出就可以看出来。

    (2) 其次,在全特化版本中,我们调用的是成员函数swap,从控制台输出也可以看出来。

    (3) 最后,我们在成员函数中有一句:using std::swap,表明:

    • 在这个函数中如果调用swap,那就会调用std命名空间下的swap.
    • 而我们接下来调用swap的时候,参数是WidgetImpl类型的指针,而不是Widget,所以编译器会调用缺省的swap函数,不会调用全特化版本的。

    由此,我们就完全实现了一个全特化版本的swap函数并弄清了此例中缺省swap,全特化swap,成员函数swap的调用顺序。

    假如Widget是一个类模板,还能特化吗?

    如下:

    template<typename T>
    class WidgetImpl
    {
        ...
    };
    template<typename T>
    class Widget
    {
      ...  
    };
    

    现在我们要将这个特化类模板,让编译器为这个类模板施行我们所写的swap函数(其实这是不正确的行为):

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

    这是一个偏特化的行为。但是上述的做法是不合法的。原因是:

    C++只允许对类模板(class template)做偏特化,在函数模板(function template)身上做偏特化是行不通的,所以这段代码无法通过编译。

    偏特化一个类模板的方法

    如果我们要偏特化一个swap函数,原则上是行不通的,因为:

    C++标准委员会禁止我们膨胀std中已经声明好的东西。客户可以全特化std内的template,但是不可以添加新的template或class或function或其他任何东西到std里头。

    解决之道

    (1) 既然不可以在std里面膨胀已有的template,那么我们就不在里面写,我们可以用我们自己的命名空间,就用WidgetStuff吧。并且在我们自己的命名空间内写一个non-member的swap.这样这个命名空间内的所有人都可以调用我们写的了。

    (2) 再就是无法对函数模板做偏特化,所以我们要取消这样的做法,就直接重载一个swap。

    做法如下:

    namespace WidgetStuff
    {
    template<typename T>
    class Widget
    {
        ...     // 省略
    };
    template<typename T>
    void swap(Widget<T> &a, Widget<T> &b) // 注意swap后面并没有<Widget T>哦
    {
        a.swap(b);
    }
    }
    

    这样子,我们的代码就可以同时处理classes,classes template两种情况了。我们要特别特别注意的是:即使在namespace WidgetStuff之中写了一个swap,它调用了成员函数swap,而成员函数swap要置换两个指针,最最简单的方式是调用std::swap这个缺省的版本!所以using std::swap这一行代码在成员函数的实现中就显得额外重要!

    void Widget::swap(Widget &other)
    {
        using std::swap;// 非常非常重要
        swap(pImpl,other.pImpl);
    }
    

    作者总结

    当std::swap对你的类型效率不高的时候,提供一个swap成员函数,并确定这个函数不抛出异常。

    如果你提供一个member swap,也该提供一个non-member swap用来调用前者,对于classes请特化std::swap.(就是我们例子的全特化)。

    调用swap时应针对std::swap然后使用using声明式,然后调用swap并且不带任何的“命名空间资格修饰”。

    为“用户自定义类型”进行std template全特化是好的,但是千万不要尝试在std内加入某些对std而言是全新的东西。

  • 相关阅读:
    Java String字符串补0或空格
    oracle查看当前用户权限
    plsql developer 导出导入存储过程和函数
    扩展jQuery easyui datagrid增加动态改变列编辑的类型
    jQueryEasyUI Messager基本使用
    combox源码解析
    Solr -- Solr Facet 2
    Solr -- Solr Facet 1
    nginx -- 安装配置Nginx
    DOS
  • 原文地址:https://www.cnblogs.com/love-jelly-pig/p/9679067.html
Copyright © 2011-2022 走看看