Effective C++ 学习笔记 条款25 考虑写出一个不抛异常的swap函数

swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程(exception-safe programming,见条款29)的脊柱,以及用来处理自我赋值可能性(见条款11)的一个常见机制。由于swap如此有用,适当的实现很重要。然而在非凡的重要性之外它也带来了非凡的复杂度。本条款探讨这些复杂度及因应之道。

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

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

只要类型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=的一般性实现细节,见条款10、11、12*pImpl = *(rhs.pImpl);// ...}// ...private:WidgetImpl *pImpl;    // 指针,所指对象内含Widget数据
};

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

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

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

这个函数一开始的template<>表示它是std::swap的一个全特化(total template specialization)版本,函数名称之后的<Widget>表示这一特化版本系针对“T是Widget”而设计。换句话说当一般性的swap template施行于Widget身上便会启用这个版本。通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的class(例如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);    // 若要置换Widget就置换其pImpl指针}// ...
};namespace std
{template<> void swap<Widget>(Widget &a, Widget &b)    // 修订后的std::swap特化版本{a.swap(b);    // 若要置换Widget,调用其swap成员函数}
}

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

然而假设Widget和WidgetImpl都是class template而非class,也许我们可以试试将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 template偏特化,在function template身上偏特化是行不通的。这段代码不该通过编译(虽然有些编译器错误地接受了它)。

当你打算偏特化一个function template时,惯常做法是简单地为它添加一个重载版本,像这样:

namespace std
{// std::swap的一个重载版本(注意swap之后没有<...>),稍后会告诉你,这也不合法)template<typename T> void swap(Widget<T> &a, Widget<T> &b){a.swap(b);}
}

一般而言,重载function template没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的template,但不可以添加新的template(或class或function或其他任何东西)到std里头。std的内容完全由C++标准委员会决定,标准委员会禁止我们膨胀那些已经声明好的东西。所谓“禁止”可能会使你沮丧,其实跨越红线的程序几乎仍可以编译和执行,但它们的行为没有明确定义。如果你希望你的软件有可预期的行为,请不要添加任何新东西到std里头。

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

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

现在,任何地点的任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则(name lookup rules,更具体地说是所谓argument-dependent lookup或Koenig lookup法则)会找到WidgetStuff内的Widget专属版本。那正是我们所要的。

上面所说的Koenig lookup法则具体如下,当遇到在对象上的方法调用时:
1.首先在对象本身的类中查找该方法。

2.如果在该类中找不到该方法,则会搜索与该类及其基类所在的命名空间。

这个做法对class和class template都行得通,所以似乎我们应该在任何时候都使用它。不幸的是有一个理由使你应该为class特化std::swap(很快会描述它),所以如果你想让你的“class专属版”swap在尽可能多的语境下被调用,你需得同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。

顺带一提,如果没有像上面那样额外使用某个命名空间,上述每件事情仍然适用(也就是说你还是需要一个non-member swap来调用member swap)。但,何必在global命名空间内塞满各式各样的class、template、function、enum、enumerant、typedef名称呢?难道你对所谓“得体与适度”失去判断力了吗?

目前为止所写的每一样东西都和swap编写者有关。换位思考,从客户观点看看事情也有必要。假设你正在写一个function template,其内需要置换两个对象值:

template<typename T> void doSomething(T &obj1, T &obj2)
{// ...swap(obj1, obj2);// ...
}

应该调用哪个swap?是std既有的那个一般化版本?还是某个可能存在的特化版本?抑或是一个可能存在的T专属版本而且可能栖身于某个命名空间(但当然不可以是std)内?你希望的应该是调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本。下面是你希望发生的事:

template<typename T> void doSomething(T &obj1, T &obj2)
{using std::swap;    // 令std::swap在此函数内可用// ...swap(obj1, obj2);    // 为T型对象调用最佳swap版本// ...
}

一旦编译器看到对swap的调用它们便查找适当的swap并调用之。C++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap。如果T是Widget并位于命名空间WidgetStuff内,编译器会使用“实参取决之查找规则”(argument-dependent lookup)找出WidgetStuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using声明式让std::swap在函数内曝光。然而即便如此编译器还是比较喜欢std::swap的T专属特化版,而非一般化的那个template,所以如果你已针对T将std::swap特化,特化版会被编译器挑中。

因此,令适当的swap被调用是很容易的。需要小心的是,别为这一调用添加额外修饰符,因为那会影响C++挑选适当函数。假设你以这种方式调用swap:

std::swap(obj1, obj2);    // 这是错误的swap调用方式

这便强迫编译器只认std内的swap(包括其任何template特化),因而不再可能调用一个定义于它所处的较适当T专属版本。某些迷途程序员的确以此方式修饰swap调用式,而那正是“你的class对std::swap进行全特化”的重要原因,它使得类型专属之swap实现版本也可被这些“迷途代码”所用(这样的代码出现在某些标准程序库实现版中,如果你有兴趣不妨帮助这些代码尽可能高效运作)。

此刻,我们已经讨论过default swap、member swap、non-member swap、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(std命名空间中允许特化,而不允许重载swap)。并令它调用你的swap成员函数。

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

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

请记住:
1.当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

2.如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非template,因为template特化std::swap时属于偏特化,而template function不允许偏特化),也请特化std::swap。

3.调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。

4.为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西(如swap的重载)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/739922.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

揭秘反向代理:探索其神秘之处

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

141 Linux 系统编程18,线程,ps –Lf 进程 查看LWP,线程间共享数据,优缺点,编译加-lpthread,

一 线程概念 什么是线程 LWP&#xff1a;light weight process 轻量级的进程&#xff0c;本质仍是进程(在Linux环境下) 进程&#xff1a;独立地址空间&#xff0c;拥有PCB 线程&#xff1a;有独立的PCB&#xff0c;但没有独立的地址空间(共享) 区别&#xff1a;在于是否共…

【1688运营】如何拆解竞争对手店铺和单品数据?

关注竞争对手数据是1688运营中不可或缺的一环&#xff0c;它有助于企业更好地了解市场环境、发现市场机会、学习成功经验、预测市场变化以及提升竞争力。以下是一些建议&#xff0c;帮助你全面、深入地分析竞争对手的店铺和单品数据&#xff1a; 1、监控店铺数据 可以通过店雷…

Docker部署黑马商城项目笔记

部署后端 创建mysql目录如下&#xff0c;上传对应的文件 运行以下命令 docker run -d \--name mysql \-p 3306:3306 \-e TZAsia/Shanghai \-e MYSQL_ROOT_PASSWORD123 \-v ./mysql/data:/var/lib/mysql \-v ./mysql/conf:/etc/mysql/conf.d \-v ./mysql/init:/docker-entry…

mac redis启动,redis哨兵模式,redis集群的相关命令

Homebrew安装的软件会默认在/usr/local/Cellar/路径下 redis的配置文件redis.conf存放在/usr/local/etc路径下 cd /usr/local/Cellar/redis/7.0.10. 存在 cd /usr/local/opt/redis/bin/redis-server. 目录存在 cd /usr/local/etc/redis.conf 存在。配置文件 复制文件 cp …

深入浅出计算机网络 day.1 概论④ 计算机网络的定义和分类

不要退却&#xff0c;要绽放魅力 我的心会共鸣 和你 —— 24.3.9 一、计算机网络的定义 计算机网络早期的一个最简单定义 现阶段计算机网络的一个较好的定义 二、计算机网络的分类 按交换方式分类 按使用者分类 按传输介质分类 按覆盖范围分类 按拓扑结构分类&#xff0c;可…

2m高分辨率土地利用分类矢量数据/植被类型分布数据

土地利用数据是在根据影像光谱特征&#xff0c;结合野外实测资料&#xff0c;同时参照有关地理图件&#xff0c;对地物的几何形状&#xff0c;颜色特征、纹理特征和空间分布情况进行分析&#xff0c;建立统一解译标志的基础之上&#xff0c;依据多源卫星遥感信息&#xff0c;结…

细粒度IP定位参文2(Corr-SLG):A street-level IP geolocation method (2021年)

[2]S. Ding, F. Zhao, and X. Luo, “A street-level IP geolocation method based on delay-distance correlation and multilayered common routers,” Secur. Commun. Netw., vol. 2021, no. 1, pp. 1–10, 2021. 智能设备的地理位置可以帮助提供多媒体内容提供商和5G网络中…

数据结构中的堆(Java)

文章目录 把普通数组转换大顶堆数组堆增删改查替换堆排序 把普通数组转换大顶堆数组 该方式适用索引为0起点的堆 在堆&#xff08;Heap&#xff09;这种数据结构中&#xff0c;节点被分为两类&#xff1a;叶子节点&#xff08;Leaf Nodes&#xff09;和非叶子节点&#xff08;N…

如何防范企业内部安全威胁?

1 用户行为分析&#xff08;UEBA&#xff09; 现代化的用户行为分析产品具有多种优势功能&#xff0c;使企业能够有效地检测内部威胁。用户行为分析软件通过收集和分析来自各种来源的数据来分析和检测内部人员的可疑行为。这些来源包括网络日志和用户活动日志。通过检查这些数…

Spring事务及事务传播机制

目录 一、什么是事务 二、事务的操作 三、Spring下实现事务 1、Spring编程式事务 &#xff08;1&#xff09;事务提交 &#xff08;2&#xff09;事务回滚 2、Spring声明式事务 &#xff08;1&#xff09;无异常--提交 &#xff08;2&#xff09;异常且不捕获--回滚 …

电机特性学习

电机特性 电机堵转&#xff1a; 电机堵转的原理 玻璃升降器&#xff1a; 工作电压 升降器在 9V~16V 电压下应运行平稳,不允许有异音和卡滞现象。 工作电流 升降器的工作电流不大于 12A,堵转电流不大于 28A。 堵转 力 升降器 堵转 力应 不小于 212N。 玻璃升降器结构 电动车窗…

删除数据表

oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 删除数据表属于数据库对象的操作 drop table 表名称; 删除 emp30 表 SQL> drop table emp30;表已删除。 上面这个语句运行后&#xff0c;就会把数据表 emp30 删除 在…

java-新手笔记(枚举)

枚举&#xff08;Enumeration&#xff09;是一种特殊的类&#xff0c;用于表示固定数量的常量值。 枚举类型使得代码更加清晰&#xff0c;易于维护&#xff0c;同时也增加了类型安全。 这边使用一个枚举封装重要数据 enum Day {SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FR…

Go语言必知必会100问题-22 空切片与nil切片有区别吗?

空切片与nil切片有区别吗&#xff1f; 很多开发人员经常混淆nil切片和空切片,不清楚什么时候使用空切片什么时候使用nil&#xff0c;而有些库函数又对这两者使用进行了区分。下面先来看看它们的定义。 空切片是length为0的切片当切片等于nil时为nil切片 下面是几种不同空切片…

salesforce || System.FinalException: Record is read-only Trigger.

以下是错误代码示范 会报错:System.FinalException: Record is read-only Trigger. trigger MoneyTrigger on Money__c (after insert, after update) {Date startDay = Date.newInstance(2023,12,26);if(Trigger.isAfter && Trigger.isInsert){for (Money__c moneys…

一起玩儿3D打印机——01 3D打印机初探

摘要&#xff1a;本文介绍3D打印技术的基本知识以及3D打印机的结构分类和组成 3D打印技术这几年得到了快速的发展&#xff0c;在一些特定领域和特定行业逐渐被越来越多的人所接受并大量使用。与之相关的供应链和产业链也日益壮大&#xff0c;并形成了完善的体系。 这次之所以…

EF框架常见异常处理汇总

EF框架使用常见异常处理 同一个实体被多次tracked的问题Json字符串转换的对象赋值时 同一个实体被多次tracked的问题 错误提示 The instance of entity type ‘XXXX’ cannot be tracked because another instance with the same key value for {‘Key’} is already being tr…

蓝桥杯刷题7

目录 1. 字母数 2. 列名 3. 大乘积 4. 最大连通 5. 星期几 1. 字母数 public class Main {public static void main(String[] args) {int num 2023;while(true) {String mInteger.toString(num,16);if(m.matches("^[a-f]$")){System.out.println(num);break;}n…

Linux环境安装配置JDK11

文章目录 第一步&#xff1a;上传JDK11安装包第二步&#xff1a;解压安装包第三步&#xff1a;修改环境变量第四步、测试是否安装成功总结 JDK 11 是 Java 开发工具包&#xff08;Java Development Kit&#xff09;的一个版本&#xff0c;它是由 Oracle 公司开发和发布的。以下…