为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

C++箴言:考虑支持不抛异常的swap

2012-09-15 7页 pdf 177KB 6阅读

用户头像

is_265274

暂无简介

举报
C++箴言:考虑支持不抛异常的swap C++箴言:考虑支持不抛异常的 swap swap 是一个有趣的函数。最早作为 STL 的一部分被引入,后来它成为异常安全编程 (exception-safe programming)的支柱和压制自赋值可能性的通用机制。因为 swap 太有用 了,所以正确地实现它非常重要,但是伴随它的不同寻常的重要性而来的,是一系列不同寻 常的复杂性。在本文中,我们就来研究一下这些复杂性究竟是什么样的以及如何对付它们。 交换两个对象的值就是互相把自己的值送给对方。缺省情况下,通过标准的交换算法来 实现交换是非常成熟的技术。典型的实现完全...
C++箴言:考虑支持不抛异常的swap
C++箴言:考虑支持不抛异常的 swap swap 是一个有趣的函数。最早作为 STL 的一部分被引入,后来它成为异常安全编程 (exception-safe programming)的支柱和压制自赋值可能性的通用机制。因为 swap 太有用 了,所以正确地实现它非常重要,但是伴随它的不同寻常的重要性而来的,是一系列不同寻 常的复杂性。在本文中,我们就来研究一下这些复杂性究竟是什么样的以及如何对付它们。 交换两个对象的值就是互相把自己的值送给对方。缺省情况下,通过的交换算法来 实现交换是非常成熟的技术。典型的实现完全符合你的预期: namespace std { template // typical implementation of std::swap; void swap(T& a, T& b) // swaps a’s and b’s values { T temp(a); a = b; b = temp; } } 只要你的类型支持拷贝(通过拷贝构造函数和拷贝赋值运算符),缺省的 swap 实现就 能交换你的类型的对象,而不需要你做任何特别的支持工作。 可是,缺省的 swap 实现可能不那么酷。它涉及三个对象的拷贝:从 a 到 temp,从 b 到 a,以及从 temp 到 b.对一些类型来说,这些副本全是不必要的。对于这样的类型,缺 省的 swap 就好像让你坐着快车驶入小巷。 这样的类型中最重要的就是那些主要由一个指针组成的类型,那个指针指向包含真正数 据的另一种类型。这种设计方法的一种常见的表现形式是 "pimpl idiom"("pointer to implementation")。一个使用了这种设计的 Widget 类可能就像这样: class WidgetImpl { // class for Widget data; public: // details are unimportant ... private: int a, b, c; // possibly lots of data - std::vector v; // expensive to copy! ... }; class Widget { // class using the pimpl idiom public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) // to copy a Widget, copy its { // WidgetImpl object. For ... // details on implementing *pImpl = *(rhs.pImpl); // operator= in general, ... // see Items 10, 11, and 12. } ... private: WidgetImpl *pImpl; // ptr to object with this }; // Widget’s data 为了交换这两个 Widget 对象的值,我们实际要做的就是交换它们的 pImpl 指针,但 是缺省的交换算法没有办法知道这些。它不仅要拷贝三个 Widgets,而且还有三个 WidgetImpl 对象,效率太低了。一点都不酷。 当交换 Widgets 的是时候,我们应该告诉 std::swap 我们打算做什么,执行交换的方 法就是交换它们内部的 pImpl 指针。这种方法的正规说法是:针对 Widget 特化 std::swap (specialize std::swap for Widget)。下面是一个基本的想法,虽然在这种形式下它还不能通 过编译: namespace std { template<> // this is a specialized version void swap(Widget& a, // of std::swap for when T is Widget& b) // Widget; this won’t compile { swap(a.pImpl, b.pImpl); // to swapWidgets, just swap } // their pImpl pointers } 这个函数开头的 "template<>" 表明这是一个针对 std::swap 的完全模板特化(total template specialization)(某些书中称为“full template specialization”或“complete template specialization”——译者注),函数名后面的 "" 表明特化是在 T 为 Widget 类型时 发生的。换句话说,当通用的 swap 模板用于 Widgets 时,就应该使用这个实现。通常, 我们改变 std namespace 中的内容是不被允许的,但允许为我们自己创建的类型(就像 Widget)完全特化标准模板(就像 swap)。这就是我们现在在这里做的事情。 可是,就像我说的,这个函数还不能编译。那是因为它试图访问 a 和 b 内部的 pImpl 指针,而它们是 private 的。我们可以将我们的特化声明为友元,但是惯例是不同的:让 Widget 声明一个名为 swap 的 public 成员函数去做实际的交换,然后特化 std::swap 去 调用那个成员函数: class Widget { // same as above, except for the public: // addition of the swap mem func ... void swap(Widget& other) { using std::swap; // the need for this declaration // is explained later in this Item swap(pImpl, other.pImpl); // to swapWidgets, swap their } // pImpl pointers ... }; namespace std { template<> // revised specialization of void swap(Widget& a, // std::swap Widget& b) { a.swap(b); // to swap Widgets, call their } // swap member function } 这个不仅能够编译,而且和 STL 容器保持一致,所有 STL 容器都既提供了 public swap 成员函数,又提供了 std::swap 的特化来调用这些成员函数。 可是,假设 Widget 和 WidgetImpl 是类模板,而不是类,或许因此我们可以参数化存 储在 WidgetImpl 中的数据类型: template class WidgetImpl { ... }; template class Widget { ... }; 在 Widget 中加入一个 swap 成员函数(如果我们需要,在 WidgetImpl 中也加一个) 就像以前一样容易,但我们特化 std::swap 时会遇到麻烦。这就是我们要写的代码: namespace std { template void swap >(Widget& a, // error! illegal code! Widget& b) { a.swap(b); } } 这看上去非常合理,但它是非法的。我们试图部分特化(partially specialize)一个函数 模板(std::swap),但是尽管 C++ 允许类模板的部分特化(partial specialization), 但不允许函数模板这样做。这样的代码不能编译(尽管一些编译器错误地接受了它)。 当我们想要“部分特化”一个函数模板时,通常做法是简单地增加一个重载。看起来就 像这样: namespace std { template // an overloading of std::swap void swap(Widget& a, // (note the lack of "<...>" after Widget& b) // "swap"), but see below for { a.swap(b); } // why this isn’t valid code } 通常,重载函数模板确实很不错,但是 std 是一个特殊的 namespace,规则对它也有特 殊的待遇。它认可完全特化 std 中的模板,但它不认可在 std 中增加新的模板(也包括类, 函数,以及其它任何东西)。std 的内容由 C++ 标准化委员会单独决定,并禁止我们对他们 做出的决定进行增加。而且,禁止的方式使你无计可施。打破这条禁令的程序差不多的确可 以编译和运行,但它们的行为是未定义的。如果你希望你的软件有可预期的行为,你就不应 该向 std 中加入新的东西。 因此该怎么做呢?我们还是需要一个方法,既使其他人能调用 swap,又能让我们得到 更高效的模板特化版本。答案很简单。我们还是声明一个非成员 swap 来调用成员 swap, 只是不再将那个非成员函数声明为 std::swap 的特化或重载。例如,如果我们的 Widget 相 关机能都在 namespaceWidgetStuff 中,它看起来就像这个样子: namespace WidgetStuff { ... // templatized WidgetImpl, etc. template // as before, including the swap class Widget { ... }; // member function ... template // non-member swap function; void swap(Widget& a, // not part of the std namespace Widget& b) { a.swap(b); } } 现在,如果某处有代码使用两个 Widget 对象调用 swap,C++ 的名字查找规则(以参 数依赖查找(argument-dependent lookup)或 Koenig 查找(Koenig lookup)著称的特定规 则)将找到 WidgetStuff 中的 Widget 专用版本。而这正是我们想要的。 这个方法无论对于类模板还是对于类都能很好地工作,所以看起来我们应该总是使用 它。不幸的是,此处还是存在一个需要为类特化 std::swap 的动机(过一会儿我会讲到它), 所以如果你希望你的 swap 的类专用版本在尽可能多的上下文中都能够调用(而你也确实 这样做了),你就既要在你的类所在的 namespace 中写一个非成员版本,又要提供一个 std::swap 的特化版本。 顺便提一下,如果你不使用 namespaces,上面所讲的一切依然适用(也就是说,你还 是需要一个非成员 swap 来调用成员 swap),但是你为什么要把你的类,模板,函数,枚 举(此处作者连用了两个词(enum, enumerant),不知有何区别——译者注)和 typedef 名 字都堆在全局 namespace 中呢?你觉得合适吗? 迄今为止我所写的每一件事情都适用于 swap 的作成者,但是有一种状况值得从客户 的观点来看一看。假设你写了一个函数模板来交换两个对象的值: template void doSomething(T& obj1, T& obj2) { ... swap(obj1, obj2); ... } 哪一个 swap 应该被调用呢?std 中的通用版本,你知道它必定存在;std 中的通用版 本的特化,可能存在,也可能不存在;T 专用版本,可能存在,也可能不存在,可能在一 个 namespace 中,也可能不在一个 namespace 中(但是肯定不在 std 中)。究竟该调用哪 一个呢?如果 T 专用版本存在,你希望调用它,如果它不存在,就回过头来调用 std 中的 通用版本。如下这样就可以符合你的希望: template void doSomething(T& obj1, T& obj2) { using std::swap; // make std::swap available in this function ... swap(obj1, obj2); // call the best swap for objects of type T ... } 当编译器看到这个 swap 调用,他会寻找正确的 swap 版本来调用。C++ 的名字查找 规则确保能找到在全局 namespace 或者与 T 同一个 namespace 中的 T 专用的 swap。(例 如,如果 T 是 namespaceWidgetStuff 中的 Widget,编译器会利用参数依赖查找 (argument-dependent lookup)找到 WidgetStuff 中的 swap。)如果 T 专用 swap 不存在, 编译器将使用 std 中的 swap,这归功于此函数中的 using 声明使 std::swap 在此可见。尽 管如此,相对于通用模板,编译器还是更喜欢 T 专用的 std::swap 的特化,所以如果 std::swap 对 T 进行了特化,则特化的版本会被使用。 得到正确的 swap 调用是如此地容易。你需要小心的一件事是不要对调用加以限定,因 为这将影响 C++ 确定该调用的函数,如果你这样写对 swap 的调用, std::swap(obj1, obj2); // the wrong way to call swap 这将强制编译器只考虑 std 中的 swap(包括任何模板特化),因此排除了定义在别处 的更为适用的 T 专用版本被调用的可能性。唉,一些被误导的程序员就是用这种方法限定 对 swap 的调用,这也就是为你的类完全地特化 std::swap 很重要的原因:它使得以这种 被误导的方式写出的代码可以用到类型专用的 swap 实现。(这样的代码还存在于现在的一 些标准库实现中,所以它将有利于你帮助这样的代码尽可能高效地工作。) 到此为止,我们讨论了缺省的 swap,成员 swaps,非成员 swaps,std::swap 的特化 版本,以及对 swap 的调用,所以让我们一下目前的状况。 首先,如果 swap 的缺省实现为你的类或类模板提供了可接受的性能,你不需要做任何 事。任何试图交换你的类型的对象的人都会得到缺省版本的支持,而且能工作得很好。 第二,如果 swap 的缺省实现效率不足(这几乎总是意味着你的类或模板使用了某种 pimpl idiom 的变种),就按照以下步骤来做: 提供一个能高效地交换你的类型的两个对象的值的 public 的 swap 成员函数。出于我 过一会儿就要解释的动机,这个函数应该永远不会抛出异常。 在你的类或模板所在的同一个 namespace 中提供一个非成员的 swap。用它调用你的 swap 成员函数。 如果你写了一个类(不是类模板),就为你的类特化 std::swap。用它也调用你的 swap 成员函数。 最后,如果你调用 swap,请确保在你的函数中包含一个 using 声明使 std::swap 可 见,然后在调用 swap 时不使用任何 namespace 限定条件。 唯一没有解决的问题就是我的警告——绝不要让 swap 的成员版本抛出异常。这是因为 swap 的非常重要的应用之一是为类(以及类模板)提供强大的异常安全(exception-safety) 保证。这项技术基于 swap 的成员版本绝不会抛出异常的假设。这一强制约束仅仅应用在成 员版本上!它不能够应用在非成员版本上,因为 swap 的缺省版本基于拷贝构造和拷贝赋值, 而在通常情况下,这两个函数都允许抛出异常。如果你写了一个 swap 的自定义版本,那么, 典型情况下你是为了提供一个更有效率的交换值的方法,你也要保证这个方法不会抛出异 常。作为一个一般规则,这两种 swap 的特型将紧密地结合在一起,因为高效的交换几乎总 是基于内建类型(诸如在 pimpl idiom 之下的指针)的操作,而对内建类型的操作绝不会 抛出异常。 Things to Remember ·如果 std::swap 对于你的类型来说是低效的,请提供一个 swap 成员函数。并确保 你的 swap 不会抛出异常。 ·如果你提供一个成员 swap,请同时提供一个调用成员 swap 的非成员 swap。对于类 (非模板),还要特化 std::swap。 ·调用 swap 时,请为 std::swap 使用一个 using 声明,然后在调用 swap 时不使用 任何 namespace 限定条件。 ·为用户定义类型完全地特化 std 模板没有什么问题,但是绝不要试图往 std 中加入 任何全新的东西。
/
本文档为【C++箴言:考虑支持不抛异常的swap】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索