考虑写出一个不抛异常的swap函数——条款25

2023-10-29

        swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programing,见条款29)的脊柱,以及用来处理自我赋值可能性(见条款11)的一个常见机制。由于swap如此有用,适当的实现很重要。

        所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。其典型实现:

namespace std {
	template<typename T>      // std::swapd的典型实现;
	void swap(T& a, T& b)      // 置换a和b的值
	{
		T temp(a);
		a = b;
		b = temp(b);
	}
}

        只要类型T支持copying(通过copy构造函数和copy assignment操作符完成),缺省的swap实现代码就会帮你置换类型为T的对象,你不需要为此另外再做任何工作。

        这个缺省的swap实现涉及三个对象复制:a复制到temp,b复制到a,以及temp复制到b。但是对某些类型而言,这些复制动作无一必要;对它们而言swap缺省行为等于是把高速铁路铺设在慢速小巷弄内。

        其中最主要的就是“以指针指向一个对象,内含真正数据”的那种类型。这种设计常见表现形式是所谓“pimpl手法”(pimpl是“pointer to implementation”的缩写,见条款31)。如果以这种手法设计Widget class,看起来会像这样:

class WidgetImpl {            // 针对Widget数据而设计的class
public:         
	...                       // 细节不重要
private:
	int a, b, c;              // 可能有许多数据
	std::vector<double> v;    // 意味复制时间很长
	...
};

class Widget {                              // 这个class使用pimpl手法
public:
	Widget(const Widget& rhs);
	Widget& operator=(const Widget& rhs)    // 复制Widget时,令它复制其WidgetImpl对象。
	{
		...                                 // 关于operator=的一般性实现
		*pImpl = *(rhs.pImpl);              // 细节,见条款10,11和12
		...
	}
	...
private:
	WidgetImpl* pImpl;                      // 指针,所指对象内含Widget数据
};

        一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的swap算法不知道这一点。它不只复制是三个Widgets,还复制三个WidgetImpl对象。非常缺乏效率!

        我们希望能够告诉std::swap:当Widgets被置换时真正该做的是置换其内部的pImpl指针。确切实践这个思路的一个做法是:将std::swap针对Widget特化。下面是基本构想,但目前这个形式无法通过编译:

namespace std {
	template<>      // 这是std::swapd针对“T是Widget”的特化版本;
	void swap<Widget>(Widget& a, Widget& b)      // 目前无法通过编译
	{ 
		swap(a.pImpl, b.pImpl);                  // 置换Widgets时只置换它们的pImpl指针就好
	}
}

        这个函数一开始的“template<>”表示它是std::swap的一个全特化(total template specialization)版本,函数名称之后的“<Widget>”表示这一特化版本针对“T是Widget”而设计。换句话说当一般性的swap template施行于Widgets身上便会启用这个版本。通常我们不被允许改变std命名空间内的任何东西,但可以为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如Widget)。以上作为正是如此。

        但是一如稍早我说,这个函数无法通过编译。因为它企图访问a和b内的pImpl指针,而那却是private。我们可以将这个特化版本声明为friend,但和以往的规矩不太一样:我们令Widget声明为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该成员函数:

class Widget {                              // 与前相同,唯一差别是增加swap函数
public:
	...
	void swap(Widget& other)
	{
		using std::swap;                    // 这个声明之所以必要,稍后解释。
		swap(pImpl, other.pImpl);           // 若要置换Widgets就置换其pImpl指针。
	}
	...
};
namespace std {
	template<>      // 修订后的std::swap特化版本
	void swap<Widget>(Widget& a, Widget& b)     
	{ 
		swap(a.pImpl, b.pImpl);                  // 若要置换Widgets,调用其swap成员函数
	}
}

       这种做法不只能通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。

        然而假设Widget和WidgetImpl都是class templates而非classes,也许我们可以试试将WidgetImpl内的数据类型加以参数化:

template<typename T>
class WidgetImpl { ... };

template<typename T>
class Widget { ... };

        在Widget内(以及WidgetImpl内,如果需要的话)放个swap成员函数就像以往一样简单,但我们却在特化std::swap时遇上乱流。我们想写成这样:

namespace std {
	template<typename T>
	void swap< Widget<T> >(Widget<T>& a, Widget<T>& b) {   // 错误!不合法!
		a.swap(b);
	}
}

        看起来合情合理,却不合法。我们企图偏特化(partially specialize)一个function template(std::swap),但C++只允许对class templates偏特化,在function templates身上偏特化是行不通的。

namespace std {
	template<typename T>                      // std::swap的一个重载版本
	void swap(Widget<T>& a, Widget<T>& b) {   // 注意“swap”之后没有“<...>”, 但即使这样也不合法。
		a.swap(b);
	}
}

        一般而言,重载function templates没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可用全特化std内的templates,但不可以添加新的templates(或classes或functions或其它任何东西)到std里头。

         那该如何是好?毕竟我们总是需要一个办法让其他人调用swap时能够取得我们提供的较高效的template特定版本。答案很简单,我们还是声明一个non-member swap让它调用member swap,但不再将那个non-member声明为std::swap的特化版本或重载版本。假设Widget的所有相关机能都被置于命名空间WidgetStuff内,整个结果看起来便像这样:

namespace WidgetStuff {
	...                                       // 模板化的WidgetImpl等等。
	template<typename T>                      // 同前,内含swap成员函数。
	class WidgetImpl { ... };
	
	...
	template<typename T>                      // non-member swap函数;
	void swap(Widget<T>& a, Widget<T>& b) {   // 这里并不属于std命名空间。
		a.swap(b);
	}
}

        这个做法对classes和class templates都行得通,所以似乎我们应该任何时候都使用它。

        此刻,我们已经讨论过default swap、member swaps、non-member swaps、std::swap特化版本、以及对swap的调用,现在让我把整个形势做个总结。

        首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。

        其次,如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:

1. 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。稍后我将解释,这个函数绝不该抛出异常。

2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。

3. 如果你正编写一个class(而非class template),为你的class 特化std::swap。并令它调用你的swap成员函数。

        最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace,赤裸裸地调用swap。

        唯一还未明确的是我的劝告:成员版swap绝不可能抛出异常。那是因为swap的一个最好的应用是帮助classes(和class templates)提供强烈的异常安全性(exception-safety)保障。条款29对此主题提供了所有细节,但此技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施行于成员版!因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而一般情况下两者都允许抛出异常。因此当你写下一个自定版本的swap,往往提供的不只是高效置换对象值的办法,而且不抛出异常。一般而言这两个swap特性是连在一起的,因为高效率的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。

 

请记住

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内假如某些对std而言全新的东西。

 

个人心得:

        从该条款中可以看到,swap函数调用时,我们传递的对象进行交换时是采用深复制的手段,为了提高效率,改成用浅复制的方式,即交换指针对象。

        如果对象非常大,采用深复制就很慢,要重新申请内存,然后赋值等一系列操作,如果只复制指针对象就快得多,因为跟对象本身大小没什么关系,不过浅复制容易出现悬垂指针的问题。所以实际项目开发过程中需要视情况而定。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

考虑写出一个不抛异常的swap函数——条款25 的相关文章

  • Effective C++

    条款01 视C 为一个语言联邦 将 视为一个由相关语言组成的联邦而非单一语言 条款02 尽量以const enum inline替换 define define处理与预处理阶段 而非编译阶段 因此此条款也可称为 宁可以编译器替换预处理器比较
  • Effective C++学习笔记——宁以传引用替换传值

    目录 一 传值效率可能会很低 二 传值可能发生割裂问题 三 适用于传值的情况和注意事项 相关博客 C 引用知识归纳 一 传值效率可能会很低 我们假设有这样两个类 class Human public string name string s
  • 编写new和delete时需固守常规——条款51

    条款50已解释什么时候你会想要写个自己的operator new和operator delete 但并没有解释当你那么做时必须遵守什么规则 这些规则不难奉行 但其中一些并不直观 所以知道它们究竟是些什么很重要 让我们从operator ne
  • 考虑写出一个不抛异常的swap函数——条款25

    swap是个有趣的函数 原本它只是STL的一部分 而后成为异常安全性编程 exception safe programing 见条款29 的脊柱 以及用来处理自我赋值可能性 见条款11 的一个常见机制 由于swap如此有用 适当的实现很重要
  • 区分接口继承和实现继承——条款34

    表面上直截了当的public继承概念 经过严密的检查之后 发现它由两部分组成 函数接口 function interfaces 继承和函数实现 function implementations 继承 这两种继承的差异 很像本书导读所讨论的函
  • 运用成员函数模板接受所有兼容类型——条款45

    所谓智能指针是 行为像指针 的对象 并提供指针没有的机能 例如条款13曾经提及std auto ptr和tr1 shared ptr如何能够被用来在正确时机自动删除heap based资源 STL容器的迭代器几乎总是智能指针 无疑地你不会奢
  • 《Effective C++》学习笔记——区分接口继承和实现继承

    派生类public继承自基类 其中函数均是接口继承 实现继承又分为缺省继承与强制继承 对应着虚函数与非虚函数 我们在做项目时 对于任何事物都要抱有先描述再组织的心态 因此 当描述事物为其构建框架尤其是存在继承 is a 关系时 一定要搞清楚
  • 令operator=返回一个reference to *this——条款10

    关于赋值 有趣的是你可以把它们写成连锁形式 int x y z x y z 15 同样有趣的是 赋值采用右结合律 所以上述连锁赋值被解析为 x y z 15 这里15先被赋值给z 然后其结果 更新后的z 再被赋值给y 然后其结果 更新后的y
  • 尽可能延后变量定义式的出现时间——条款26

    只要你定义了一个变量而其类型带有一个构造函数或析构函数 那么当程序控制流 control flow 到达这个变量定义式时 你便得承受构造成本 当这个变量离开其作用域时 你便得承受析构成本 即使这个变量最终未被使用 仍需耗费这些成本 所以你应
  • 在operator=中处理“自我赋值”——条款11

    自我赋值 发生在对象被赋值给自己时 class Widget Widget w w w 赋值给自己 这看起来有点愚蠢 但它合法 所以不要认定客户绝不会那么做 此外赋值动作并不总是那么可被一眼辨识出来 例如 a i a j 潜在的自我赋值 如
  • 将文件间的编译依存关系降至最低——条款31

    假设你对C 程序的某个class实现文件做了些轻微修改 注意 修改的不是class接口 而是实现 而且只改private成分 然后重新建置这个程序 并预计只花数秒就好 毕竟只有一个class被修改 你按下 Build 按钮或键入make 或
  • 条款13: 以对象管理资源

    结论 为防止资源泄漏 请使用RAII对象 它们在构造函数中获得资源并在析构函数中释放资源 两个常被使用的RAII classes分别是tr1 share ptr和auto ptr 前者通常是较佳选择 因为其copy行为比较直观 若选择aut
  • Effective C++改善程序与设计的55个具体做法笔记

    Scott Meyers大师Effective三部曲 Effective C More Effective C Effective STL 这三本书出版已很多年 后来又出版了Effective Modern C More Effective
  • 将与参数无关的代码抽离templates——条款44

    Templates是节省时间和避免代码重复的一个奇方妙法 不再需要键入20个类似的classes而每一个带有15个成员函数 你只需键入一个class template 留给编译器去具现化那20个你需要的相关classes和300个函数 cl
  • 考虑virtual函数以外的其他选择——条款35

    假设你正在写一个视频游戏软件 你打算为游戏内的人物设计一个继承体系 你的游戏术语暴力砍杀类型 剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见 你因此决定提供一个成员函数healthValue 它会返回一个整数 表示人物的健康程度 由
  • 写了placement new也要写placement delete——条款52

    placement new和placement delete并非C 兽栏中最常见的动物 如果你不熟悉它们 不要感到挫折或忧虑 回忆条款16和17 当你写一个new表达式像这样 Widget pw new Widget 共有两个函数被调用 一
  • 明智而审慎地使用多重继承——条款40

    当多重继承 multiple inheritance MI 进入设计景框 程序有可能从一个以上的base classes继承相同名称 如函数 typedef等等 那会导致较多的歧义机会 例如 class BorrowableItem 图书馆
  • 掌握 Effective C++ : 条款01

    背景 Effective C 是每个 C 程序员都应该读的经典之作 书中涵盖了 C 编程中的一系列最佳实践 包括了面向对象设计 模板 STL 异常处理等方面的内容 由于 C 的发展非常迅速 书中的某些内容可能已经过时 但依然是值得好好学习的
  • 复制对象时勿忘其每一个成分——条款12

    设计良好之面向对象系统 OO systems 会将对象的内部封装起来 只留两个函数负责对象拷贝 复制 那便是带着适切名称的copy构造函数和copy assignment操作符 我称它们为copying函数 条款5观察到编译器会在必要的时候
  • Effective C++——尽可能使用const

    const允许指定一个语义约束 也就是指定一个 不该被改动 的对象 而编译器会强制实施这项约束 只要保持某个值不变是事实 就应该说出来 以获得编译器的协助 保证不被违反 const与指针 注意const的写法 const char p p可

随机推荐

  • Matplotlib数据可视化

    e 安装及使用 安装 pip install i https mirrors aliyun com pypi simple matplotlib 使用 import Matplotlib pyplot as plt 操作需要numpy数据对
  • uni-app 布局固定头部,内容滚动

    1 代码
  • ios部分机型出现select、input等控件点击后失效不可再次点击dug

    问题描述 在昨天晚上的时候测试突然告诉我一个问题 在iphone 6s中select选择器在第一次点击后 其他的选择无法点击 整个手机都属于暂时性死机状态 问题分析 当时首先对代码进行了排查 排除是逻辑方面的问题 经过多方面验证发现只有6s
  • TensorFlow笔记:激活函数

    tf nn sigmid 函数 函数表达式 f x 1 1
  • h5+js+ajax+百度翻译API:实现翻译功能

    使用前端技术 h5 js ajax开发网页翻译功能 调用百度开放平台的API 入门级前端demo 非常详细好入手 功能为 点击Translate按钮 实现英译汉 页面如下 一 appID和key值准备 在百度开放平台https api fa
  • popState 监听浏览器切换路由

    浏览器内 popState 监听器使用 在前端开发过程中 在一些业务场景中可能会遇到监听浏览器前进 后退 控制路由等情况 我们可以使用Web API提供的popState事件来处理这些情况 提到popState 应用中 通常pushStat
  • python语法-def()详细介绍(特别全)

    1 什么是函数 在 Python 中 函数是一种可重用的代码块 用于执行特定的任务或操作 函数可以接受输入参数 并返回输出结果 从而实现模块化和封装性编程的目的 Python 中定义函数的语法如下 def function name par
  • 网络安全比赛A模块任务书

    前言 这是作者这几个月来的第一次更新文章 问就是太忙了 最近要去参加国赛 在此重新回来写文章 也不知道能写多久 就当练习了 一 A模块基础设施设置 安全加固 A 1 登录加固 1 密码策略 a 最小密码长度不少于8个字符 将密码长度最小值的
  • data1

    两数之和 给定一个整数数组 nums 和一个目标值 target 请你在该数组中找出和为目标值的那 两个 整数 并返回他们的数组下标 你可以假设每种输入只会对应一个答案 但是 你不能重复利用这个数组中同样的元素 给定 nums 2 7 11
  • vue3+ts预览和下载word,pdf,excel

    文章目录 前言 一 vue office相比其他库的优势 二 使用步骤 1 引入库 2 组件封装 3 组件使用 总结 前言 最近项目一直在做一些文档方面的相关操作 例如查看文件 做文件导出 一般的导出格式为word excel pdf 等
  • STM32 实战中的一些技术问题解决

    开发板高速USB接口延迟问题 首先是检查硬件接口布线和插槽整体有无损坏 第二是发送数据包进行测试 判断是哪种数据类型和字段会发生时延 然后对症解决问题 以上两种方式能够解决80 的延迟问题 当然 也有比较少见的疑难杂症 发现当 USB 线拔
  • P1182 数列分段 Section II

    题目描述 对于给定的一个长度为N的正整数数列 A 现要将其分成 M M N 段 并要求每段连续 且每段和的最大值最小 关于最大值最小 例如一数列 4 2 4 5 1 要分成 3 段 将其如下分段 4 2 4 5 1 第 1 段和为 6 第
  • ubuntu18.04安装显卡驱动(四种方式)

    一 引言 安装ubuntu显卡驱动根据经验来看一共有四种方法 推荐使用方法三和方法四最简单快捷 一般方法三就可以解决 方法三不可以的话再用其他办法 反正自己多试试 大不了就重装系统嘛 还有一个新系统先别配置其他东西 先安显卡驱动 根据实验室
  • stm32定时器部分 产生spwm波

    用的是野火的例程 main函数 define CLI set PRIMASK 1 define SEI set PRIMASK 0 int main void CLI SEI TIM34 PWM Init while 1 bsp pwm o
  • ICP算法思想与推导详解——什么东西“最近点”,又“迭代”了什么?

    一 什么是ICP ICP的全称是 iterative closest point 迭代最近点 它是一种点云匹配的算法 在三维重建或者视觉Slam场景中 经常需要确定某一时刻的相机位姿 相机在运动过程中 不同时刻对同一物体获取的三维点云信息是
  • docker portainer_docker随手笔记第十一节 portainer.io安装使用,比k8s简单

    docker随手笔记第一节 docker概念及安装 docker随手笔记第二节 docker常用命令解析 docker随手笔记第三节 docker构建java镜像 docker随手笔记第四节 docker安装mysql5 7 docker随
  • CSR867x — 如何修改BLE的蓝牙地址

    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XX 作 者 文化人 XX 联系方式 进群 471144274 XX 版权声明 原创文章 欢迎评论和转载 转载时能告诉我一声就最好了
  • Pandas数据排序——【按索引排序sort_index()方法、按值排序sort_value()方法】

    文章目录 按索引排序 sort index 对Series排序 对DataFrame排序 按值排序 sort value 对Series进行排序 对DataFrame进行排序 按索引排序 sort index sort index axis
  • java对象比较“==”与“equals()”详解

    文章目录 写在前面 与 equals 是两个实例对象的什么进行比较 equals 的覆盖 必然伴随着hashCode的覆盖 为什么 hashCode与equals重写原则 写在前面 本文的书写仅是个人的理解 目的加深对技术点的理解 知识积累
  • 考虑写出一个不抛异常的swap函数——条款25

    swap是个有趣的函数 原本它只是STL的一部分 而后成为异常安全性编程 exception safe programing 见条款29 的脊柱 以及用来处理自我赋值可能性 见条款11 的一个常见机制 由于swap如此有用 适当的实现很重要