【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 对象,常缺乏效率。

  • 思路

我们希望能够告诉std::swap:当Contour被置换时真正该做的是置换其内部的pImpl指针。确切实现这个思路的一个做法是:将std::swap针对Contour特化。下面是基本构想,但目前这个形式无法通过编译:
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;
}
View Code
  • 注意

构造函数如果这样写:

// 写法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;
}
View Code

结果为:

原因:

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。

posted @ 2022-07-31 21:47  湾仔码农  阅读(91)  评论(0编辑  收藏  举报