大学IT网 - 最懂大学生的IT学习网站! QQ资料交流群:367606806
当前位置:大学IT网 > C++技巧 > C++技巧:支持不抛异常的swap

C++技巧:支持不抛异常的swap

关键词:异常swap  阅读(489) 赞(10)

[摘要]本文是一个支持不抛异常的swap函数的实现,与大家分享。

  swap 是一个幽默的函数。最早作为 STL 的一局部被引入,后来它成为异常平安编程(exception-safe programming)的支柱和压制自赋值可以性的通用机制。由于 swap 太有用了,所以正确地完成它十分重要,但是随同它的不同寻常的重要性而来的,是一系列不同寻常的复杂性。在本文中,我们就来研讨一下这些复杂性终究是什么样的以及如何凑合它们。

  交流两个对象的值就是相互把本人的值送给对方。缺省状况下,经过规范的交流算法来完成交流是十分成熟的技术。典型的完成完全契合你的预期:

namespace std {

 template<typename T> // 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<double> 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>(Widget& a, // of std::swap for when T is
 Widget& b) // Widget; this won’t compile
 {
  swap(a.pImpl, b.pImpl); // to swap Widgets, just swap
 } // their pImpl pointers
}

  这个函数扫尾的 "template<>" 标明这是一个针对 std::swap 的完全模板特化(total template specialization)(某些书中称为“full template specialization”或“complete template specialization”——译者注),函数名前面的 "<Widget>" 标明特化是在 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 swap Widgets, swap their
} // pImpl pointers
...
};

namespace std {

 template<> // revised specialization of
 void swap<Widget>(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<typename T>
class WidgetImpl { ... };

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

  在 Widget 中参与一个 swap 成员函数(假定我们需求,在 WidgetImpl 中也加一个)就像以前一样容易,但我们特化 std::swap 时会遇到费事。这就是我们要写的代码:

namespace std {
 template<typename T>
 void swap<Widget<T> >(Widget<T>& a, // error! illegal code!
 Widget<T>& b)
 { a.swap(b); }
}

  这看上去十分合理,但它是合法的。我们试图局部特化(partially specialize)一个函数模板(std::swap),但是虽然 C++ 允许类模板的局部特化(partial specialization),但不允许函数模板这样做。这样的代码不能编译(虽然一些编译器错误地承受了它)。

  当我们想要“局部特化”一个函数模板时,通常做法是复杂地添加一个重载。看起来就像这样:

namespace std {

template<typename T> // an overloading of std::swap
void swap(Widget<T>& a, // (note the lack of "<...>" after
Widget<T>& 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 相关机能都在 namespace WidgetStuff 中,它看起来就像这个样子:

namespace WidgetStuff {
 ... // templatized WidgetImpl, etc.

 template<typename T> // as before, including the swap
 class Widget { ... }; // member function

 ...

 template<typename T> // non-member swap function;
 void swap(Widget<T>& a, // not part of the std namespace
 Widget<T>& 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<typename T>
void doSomething(T& obj1, T& obj2)
{
 ...
 swap(obj1, obj2);
 ...
}

  哪一个 swap 应该被调用呢?std 中的通用版本,你晓得它一定存在;std 中的通用版本的特化,可以存在,也可以不存在;T 公用版本,可以存在,也可以不存在,可以在一个 namespace 中,也可以不在一个 namespace 中(但是一定不在 std 中)。终究该调用哪一个呢?假定 T 公用版本存在,你希望调用它,假定它不存在,就回过头来调用 std 中的通用版本。如下这样就可以契合你的希望:

template<typename T>
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 是 namespace WidgetStuff 中的 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 中参与任何全新的东西。



相关评论