【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;
}
注意
构造函数如果这样写:

// 写法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。
【Effective C++】设计与声明——考虑写出一个不抛异常的swap函数的更多相关文章
- 《Effective C++》item25:考虑写出一个不抛异常的swap函数
std::swap()是个很有用的函数,它可以用来交换两个变量的值,包括用户自定义的类型,只要类型支持copying操作,尤其是在STL中使用的很多,例如: int main(int argc, _T ...
- Effective C++ -----条款25:考虑写出一个不抛异常的swap函数
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常. 如果你提供一个member swap,也该提供一个non-member swap用来调用前者.对于cla ...
- [Effective C++ --025]考虑写出一个不抛异常的swap函数
引言 在我的上一篇博客中,讲述了swap函数. 原本swap只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性. 一.swap函数 标准库的swap函数如下: namespa ...
- Effective C++:条款25:考虑写出一个不抛异常的swap函数
(一) 缺省情况下swap动作可由标准程序库提供的swap算法完毕: namespace std { template<typename T> void swap(T& a, T& ...
- EC读书笔记系列之13:条款25 考虑写出一个不抛异常的swap函数
记住: ★当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定其不抛出异常 ★若你提供一个member swap,也该提供一个non-member swap来调用前者.对于cla ...
- Effective C++ Item 25 考虑写出一个不抛异常的swap函数
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛 ...
- 【25】考虑写出一个不抛异常的swap函数
1.swap交换对象值,std提供了实现方法模版的实现,它的实现是交换对象值. namespace std { template<typename T> void swap(T& ...
- Effective C++ —— 设计与声明(四)
条款18 : 让接口容易被正确使用,不易被误用 欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误操作. 1. 明智而审慎地导入新类型对预防“接口被误用”有神奇疗 ...
- Effective C++ ——设计与声明
条款18:让接口更容易的被使用,不易误用 接口设计主要是给应用接口的人使用的,他们可能不是接口的设计者,这样作为接口的设计者就要对接口的定义更加易懂,让使用者不宜发生误用,例如对于一个时间类: cla ...
- 【C++】从零开始的CS:GO逆向分析3——写出一个透视
[C++]从零开始的CS:GO逆向分析3--写出一个透视 本篇内容包括: 1. 透视实现的方法介绍 2. 通过进程名获取进程id和进程句柄 3. 通过进程id获取进程中的模块信息(模块大小,模块地址, ...
随机推荐
- 浏览器端实现类似input限制输入两位小数,输入时光标从输入位置移动到最后
1.问题描述展示 示例代码所做限制为不允许输入字母d,其他限制规则可以根据需求自己调整,使用React编写,其他框架或原生均可根据该代码理解原理进行转变,特意使用了中文键盘可以看到输入框下面白色框闪出 ...
- 第一次blog
前言:我在大一上学期学习了c语言,然后在下学期学习了第二门语言java,因为之前c语言学的挺一般的,然后在这学期学习java感觉还是挺不简单的,要自学很多东西,在这段时间里,我学习了JAVA的基本语法 ...
- 【Oracle】使用PL/SQL快速查询出1-9数字
[Oracle]使用PL/SQL快速查询出1-9数字 简单来说,直接Recursive WITH Clauses 在Oracle 里面就直接使用WITH result(参数)即可 WITH resul ...
- 力扣628(java)-三个数的最大乘积(简单)
题目: 给你一个整型数组 nums ,在数组中找出由三个数组成的最大乘积,并输出这个乘积. 示例 1: 输入:nums = [1,2,3]输出:6示例 2: 输入:nums = [1,2,3,4]输出 ...
- 技术干货 | 使用 mPaaS 配置 SM2 国密加密指南
简介:随着移动智能终端的广泛应用,敏感信息极易被监控或盗取,给国家.企事业及个人带来极大政治.经济损失.金融和重要领域的各个企业正在逐步落实并完成国产密码改造工作.为解决客户侧因更换加密算法造成的种 ...
- 【ESSD技术解读-01】 云原生时代,阿里云块存储 ESSD 快照服务如何被企业级数据保护所集成?
简介: 本文描述了阿里云块存储快照服务基于高性能 ESSD 云盘提升快照服务性能,提供轻量.实时的用户体验及揭秘背后的技术原理.依据行业发展及云上数据保护场景,为企业用户及备份厂商提供基于快照高级特 ...
- 技术干货 | jsAPI 方式下的导航栏的动态化修改
简介: 操作指导:通过 jsAPI 实现导航栏的动态修改. 很多开发同学在接入 H5 容器后都会对容器的导航栏进行深度定制,除了 Native 的定制化之外,还有很多场景是使用到 jsAPI 的 ...
- dotnet 在 UOS 统信系统上运行 UNO 程序输入时闪烁黑屏问题
本文记录我在虚拟机内安装了 UOS 统信系统,运行 UNO 的基于 Skia 的 Gtk 应用程序时,在输入的过程中不断窗口闪黑问题 本质上说这个问题和 UNO 毫无关系,这是一个 OpenGL 硬件 ...
- Linux内核之SPI协议
SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行的行业标准,但是并没有像I2C那样有标准文档,它还有主从.可片选的特性. 图源自Serial Periphe ...
- ubuntu16下升级python3的版本--升级到3.8
ubuntu16下升级python3的版本,这里是升级到3.8. 1.首先添加安装源,在命令行输入如下命令: $ sudo add-apt-repository ppa:jonathonf/pytho ...