【Effective C++】设计与声明——考虑写出一个不抛异常的swap函数
wap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来实现自我赋值可能性的一个常见机制。所谓swap两对象值,就是将两对象的值交换。
典型实现
缺省情况下的swap动作可有标准库提供的swap算法完成:
namespace std{ template<typename T> void swap(T & a, T & b){ T temp(a); a = b; b = temp; } };
只要类型T支持拷贝(通过拷贝构造函数和拷贝运算符),缺省的swap就可以完成交换两相同类型的对象的值,你不需要为此做任何动作。
-
缺点
上面版本的实现调用了三次拷贝,对于某些类型而言,这些拷贝无一必要。其中最主要的就是“以指针指向一个对象,内含指针数据”的那种类型。这种设计的常见表现形式就是"pimpl(pointer to implementation)手法"。比如:
class ContourInfo {//用于存放轮廓的信息 public: ... private:
...//假设有很多数据,复制时间很长 int index;//索引 double area;//面积 Point2f ps[4];//最小外接矩形的四个角点 }; class Contour { public: ... private: ContourInfo* pCon;//指针,所指对象内含Contours数据 };
一旦要置换两个Contour对象的值,我们唯一要做的是交换其pimpl指针,但是缺省的swap不知道这一点。它不止复制三个Contour ,还复制三个ContourInfo 对象,常缺乏效率。
-
思路
namespace std{ template<> void swap<Contour>(Contour&a, Contour&b){ swap(a.pcon, b.pcon); // 只需要交换它们的pImpl指针就好 }; };
这个函数一开始的template<>
表示它是std::swap的一个全特化版本,函数名称后面的<Contour>表示这一特化版本是针对"T是Contour"而设计。换句话说当一般性的swap模板施行于Contour身上就会启用这个版本。通常我们不被允许改变std命名空间内的任何东西,但可以为标准模板(比如swap)制作特化版本,使它专属于我们自己的类(比如Contour).
但是这个版本无法通过编译,因为它企图访问a和b内的的private pImpl指针。
特化实现
我们可以将这个特化版本声明为友元,但是和以往不一样:我们令Contour声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该函数:
class Contour{ public: ... void swap(Contour& other){ using std::swap; swap(pcon, other.pcon); } } namespace std{ template<> void swap<Contour>(Contour&a, Contour& b){//no-memberde swap a.swap(b);//调用swap成员函数 } };
这种做法不止能够通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本。
-
示例
为深入理解,写了个例子,将下图中的轮廓按面积大小排序。
代码:
#include <iostream> #include <opencv2/opencv.hpp> using namespace std; using namespace cv; class ContourInfo{//用于存放轮廓信息 public: ContourInfo() {}; ContourInfo(int idx, double a, Point2f p[4]) { index = idx; area = a; for (int i = 0; i < 4; i++) { ps[i] = p[i]; } } double Area() { return area; } double Index() { return index; } private: int index; double area; Point2f ps[4]; }; class Contour { public: Contour(ContourInfo& con) { pCon = new ContourInfo(); *pCon = con; } Contour(const Contour& con) { pCon = new ContourInfo(); *pCon = *(con.pCon); } ~Contour() { delete pCon; } bool operator< (const Contour& con) { return pCon->Area() < con.pCon->Area(); } void swap(Contour& con) { using std::swap; swap(pCon, con.pCon); } void print() { cout << "index为" << pCon->Index() << "的轮廓面积为:" << pCon->Area() << endl; } private: ContourInfo* pCon; }; namespace std { template<> void swap<Contour>(Contour& a, Contour& b) { a.swap(b); } }; int main() { Mat src = imread("D:/Backup/桌面/1.png", 0); vector<vector<Point>> cons; findContours(src, cons, RETR_LIST, CHAIN_APPROX_SIMPLE); Point2f ps[4]; vector<Contour> contours; for (size_t i = 0; i < cons.size(); i++) { RotatedRect rec = minAreaRect(cons[i]); rec.points(ps); ContourInfo temp(i, contourArea(cons[i]), ps); Contour con(temp); contours.push_back(con); } cout << "-------------排序前------------" << endl; for (size_t i = 0; i < contours.size(); i++) { contours[i].print(); } for (size_t i = 0; i < contours.size(); i++) {//冒泡排序 for (size_t j = 0; j < contours.size() - i - 1; j++) { if (contours[j] < contours[j + 1]) continue; swap(contours[j], contours[j + 1]); } } cout << "-------------排序后------------" << endl; for (size_t i = 0; i < contours.size(); i++) { contours[i].print(); }return 0; }
-
注意
构造函数如果这样写:
// 写法1 Contour(ContourInfo& con) { pCon = &con; } Contour(const Contour& con) { pCon = con.pCon; } // 写法2 Contour(ContourInfo& con) { pCon = &con; } Contour(const Contour& con) { pCon = new ContourInfo(); *pCon = *(con.pCon); } // 写法3 Contour(ContourInfo& con) { pCon = new ContourInfo(); *pCon = con; } Contour(const Contour& con) { pCon = con.pCon; } // 写法4 Contour(ContourInfo& con) { pCon = new ContourInfo(); *pCon = con; } Contour(const Contour& con) { pCon = new ContourInfo(); *pCon = *(con.pCon); } ~Contour() { delete pCon; }
结果为:
原因:
for(...){
ContourInfo temp(i, contourArea(cons[i]), ps);
Contour con(temp);
contours.push_back(con);
}
在for循环中,temp的地址固定。写法1在构造con时,pCon永远指向第一个temp的地址,而temp的信息总是在改变,所以最后vector中每一个轮廓的信息都是最后一个轮廓的信息。写法2新建con的pCon还是永远指向固定的temp地址,但是pushback会调用copy赋值函数,copy赋值函数新建了内存存放轮廓信息。写法3每次新建的con中的pCon指向不同的地址,pushback在copy时,新的指针还是指向之前的地址。写法4才能调用delete,不然我不知道在哪里释放内存。
总结
如果swap的缺省实现码对你的类或者类模板提供可接受的效率,你不需要做任何事情。但是,如果你觉得swap缺省实现版的效率不足(那几乎总是意味着你的类或者模板使用了某种pimpl手法),试着做如下事情:
1. 提供一个public swap成员函数,让他高效地置换你的类型的两个对象值,这个函数绝不该抛出异常。non-member函数是可以抛出异常的。
2. 在你的类或者模板所在的命名空间内提供一个non-member swap,并令它调用上面的swap成员函数
3. 如果你正在编写一个类(而不是类模板),为你的类特化std::swap,并令它调用你的swap成员函数。
4. 最后,如果你调用swap,请确定包含一个using声明式,以便让你的std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。