Skip to content

Latest commit

 

History

History
143 lines (114 loc) · 4.98 KB

File metadata and controls

143 lines (114 loc) · 4.98 KB

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

swap原本只是STL的一部分,而后成为异常安全便利的脊柱,以及用来处理自我赋值可能性的一个常见机制。由于swap如此有用,适当的实现很重要。

默认情况下swap操作可由标准库提供的swap算法完成。其典型实现完全如你所想:

namespace std {
template <typename T>
void swap(T& a, T& b) {
  T = temp(a);
  a = b;
  b = temp;
}
}

只要T支持copying,默认的swap实现代码就会帮你置换类型T的对象,你不需要为此另外再做任何操作。

而需要复杂实现的是:以指针指向一个对象,内含真正数据的类型。这种设计的常见表现形式是pimpl手法(pointer to implementation)。如果以这种手法设计Widget class,看上去会像这样:

class WidgetImpl {
public:
  ...
private:
  int a, b, c;
  std::vector<double> v;
  ...
};

class Widget {
public:
  Widget(const Widget& rhs);
  Widget& operator=(const Widget& rhs) {
    ...
    *pImpl = *(rhs.pImpl);
    ...
  }
  ...
private:
  WidgetImpl* pImpl;
};

一旦要置换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但默认swap算法不知道这一点。它复制了三次Widget和WidgetImpl,的效率极低。

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

namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
  swap(a.pImpl, b.pImpl);
}
}

通常我们不能够改变std命名空间里的任何东西,但可以为标准template制造特化版本,使他专属于我们自己的class。以上便是如此。

这个函数无法通过编译,因为它企图访问a和b内的pImpl指针,而它是private的。我们可以将这个特化版本声明为friend。我们令Widget声明要给swap的public成员函数做真正的置换工作,然后将 std::swap 特化,令他调用该成员函数:

class Widget {
public:
  ...
  void swap(Widget& other) {
    using std::swap;    // 此行有必要,稍后解释
    swap(pImpl, other.Impl);
  }
  ...
}

namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
  a.swap(b_;)
}
}

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

如果Widget和WidgetImpl都是class template,我们需要为swap添加一个重载版本,因为C++不允许对函数模板进行偏特化。像这样:

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

std命名空间中,客户可以全特化template,但不可以添加新的template(包括重载)。这是一个未定义行为。

于是,我们还是可以声明一个non-member swap让它调用member swap,不再将swap声明为 std::swap 的特化或重载版本。并把Widget相关功能都放进命名空间WidgetStuff内:

namespace WidgetStuff {
...
template <typename T>
class Widget {...};
...
template <typename T> 
void swap(Widget<T>& a, Widget<T>& b) { // 不属于std命名空间
  a.swap(b);
}
}

现在,交换两个Widget对象,会找到WidgetStuff内的Widget专属版本的swap。那正是我们想要的。

现在,如何让编译器确定最合适的swap版本?使用 using std::swap ,让该函数与特化版本一起参与重载决议,编译器会选择最合适的版本执行。

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

现在我们做个总结:

首先,如果swap的默认代码对你的class提供可接受的效率,你不需要额外做任何事。

其次,如果swap的默认代码效率不足,试着做以下事情:

  1. 提供一个public swap成员函数,让它高效地置换你的类型的两个值。这个函数绝不该抛出异常。
  2. 在你的class或template所在命名空间内提供一个non-member swap,并令他调用上述swap成员函数。
  3. 如果你正在编写一个class,为你的class特化 std::swap,并令他调用你的swap成员函数。

最后,如果你调用swap,请确认包含一个 using 声明式,让 std::swap 参与重载决议,然后不加任何namespace修饰符,直接调用 swap

请记住

  • std::swap 对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class,也请特化 std::swap
  • 调用swap时应针对 std::swapusing 声明式,然后调用swap而不加任何namespace修饰符。
  • 为用户自定义类型进行std template全特化是好的,但千万不要尝试在std内部加入某些对std而言全新的东西。