四、设计和声明--条款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而言是全新的东西。

原文地址:https://www.cnblogs.com/love-jelly-pig/p/9679067.html