四、设计和声明--条款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版本。
现在我们考虑如果写函数里面真实的交换操作的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而言是全新的东西。