考虑支持一个 non-throwing swap(不抛异常的 swap)

SWAP 是一个有趣的函数。最早作为 STL 的构件被引入,后来它成为 exception-safe programming(异常安全编程)的支柱和压制自赋值可能性的通用机制。因为 swap 太有用,所以适当地实现它非常重要,但伴随它的不同寻常的重要性而来的,是一系列不同寻常的复杂性。

 

swap(交换)两个 objects 的值就是互相把自己的值送给对方。缺省情况下,经由标准的 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;
  }
}

 

只要我们的类型支持拷贝(经由 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)),缺省的 swap 实现就能交换我们的类型的 objects,而不需要做任何特别的支持工作。

 

然而,缺省的 swap 实现可能效率不好。它涉及三个 objects 的拷贝:从 a temp,从 b a,以及从temp b。对一些类型来说,这些副本全是不必要的。

 

这样的类型中最重要的就是那些主要由一个指针组成的类型,那个指针指向包含真正数据的另一种类型。这种设计方法的一种常见的表现形式是 "pimpl idiom""pointer to implementation" —)。一个使用了这种设计的 Widget class 可能就像这样:

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 objects 的值,我们实际要做的全部就是交换它们的 pImpl pointers(指针),但是缺省的 swap 算法没有办法知道这些。它不仅要拷贝三个 Widgets,而且还有三个 WidgetImpl objects,效率太低。

 

我们想要做的就是告诉 std::swap 当交换 Widgets 时,执行交换的方法就是 swap 它们内部的 pImpl pointers。这种方法的正规说法是:specialize std::swap for Widget(针对 Widget 特化 std::swap)。下面是一个基本的想法,虽然在这种形式下它还不能通过编译:

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>" 表明这个 specialization(特化)是在 T Widget 时发生的。换句话说,当通用的 swap template(模板)用于 Widgets 时,就应该使用这个实现。通常,我们不被允许改变 std namespace 中的内容的,但我们被允许为我们自己创建的类型(比如 Widget)完全地特化标准模板(比如 swap)。

 

可是,这个函数还不能编译。那是因为它试图访问 a b 内部的 pImpl pointers(指针),而它们是 private(私有)的。我们可以将我们的 specialization(特化)声明为一个 friend(友元),但是惯例是不同的:它让 Widget 声明一个名为 swap public member function(公有成员函数)去做实际的交换,然后特化 std::swap 去调用那个 member function(成员函数):

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 containers(容器)保持一致,所有 STL containers(容器)都既提供了 public swap member functions(公有 swap 成员函数),又提供调用这些 member functions(成员函数)的 std::swap specializations(特化)。

 

可是,假设 Widget WidgetImpl class templates(类模板),而不是 classes(类),或许因此我们可以参数化存储在 WidgetImpl 中的数据的类型:

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

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

 

Widget 中加入一个 swap member function(成员函数)(如果我们需要,在 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(部分特化)一个 function template(函数模板)(std::swap),但是尽管 C++ 允许 class templates(类模板)的 partial specialization(部分特化),但不允许 function templates(函数模板)这样做。这样的代码不能编译(尽管一些编译器错误地接受了它)。

 

当我们想要 "partially specialize"(“部分地特化”)一个 function templates(函数模板)时,通常做法是简单地增加一个 overload(重载)。看起来就像这样:

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
}

 

通常,重载 function templates(函数模板)确实很不错,但是 std 是一个特殊的 namespace,而且管理它的规则也是特殊的。它认可完全地特化 std 中的 templates(模板),但它不认可在 std 中增加 new templates(新的模板)(或 classes,或函数,或其它任何东西)。std 的内容由 C++ 标准化委员会单独决定,并禁止我们对他们做出的决定进行增加。而且,禁止的方式使你无计可施。打破这条禁令的程序差不多的确可以编译和运行,但它们的行为是未定义的。如果你希望你的软件有可预期的行为,你就不应该向 std 中加入新的东西。

 

那么该怎么做呢?我们依然需要一个方法,既使其他人能调用 swap,又能让我们得到更高效的 template-specific(模板专用)版本。答案很简单。我们依然声明一个 non-member(非成员)swap 来调用 member(成员)swap,只是不再将那个 non-member(非成员)声明为 std::swap 的 specialization(特化)或 overloading(重载)。例如,如果我们的 Widget-related(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 对象调用 swapC++ 的 name lookup rules(名字查找规则)(特指以 argument-dependent lookup(参数依赖查找)或 Koenig lookupKoenig 查找)著称的规则)将找到 WidgetStuff 中的 Widget-specific(Widget 专用)版本。而这正是我们想要的。

 

这个方法无论是对于 classes(类)还是对于 class templates(类模板)都能很好地工作,所以看起来我们应该总是使用它。不幸的是,此处还是存在一个需要为 classes 特化 std::swap 的动机,所以如果你想让你的 swap 的 class-specific(类专用)版本在尽可能多的上下文中都能够调用(而你也确实这样做了),你就既要在你的 class 所在的 namespace 中写一个 non-member(非成员)版本,又要提供一个 std::swap 的 specialization(特化)。

 

顺便提一下,如果你不使用 namespaces(名字空间),上面所讲的一切依然适用(也就是说,你还是需要一个 non-member(非成员)swap 来调用 member(成员)swap),但是你为什么要把你的 class(类),template(模板),function(函数),enum,enumerant(枚举)和 typedef names 都堆在 global namespace(全局名字空间)中呢?你觉得合适吗?

 

迄今为止所写的每一件事情都是适用于 swap 的作成者的,但是有一种状况值得从客户的观点来看一看。假设你在需要交换两个 objects 的值的地方写了一个 function template(函数模板):

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

 

哪一个 swap 会被调用呢?std 中的通用版本,你知道它必定存在;std 中的通用版本的一个 specialization(特化),可能存在,也可能不存在;T-specific(T 专用)版本,可能存在,也可能不存在,可能在一个 namespace(名字空间)中,也可能不在一个 namespace(名字空间)中(但是肯定不在 std 中)。究竟该调用哪一个呢?如果 T-specific(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++ 的 name lookup rules(名字查找规则)确保能找到在 global namespace(全局名字空间)或者与 T 同一个 namespace(名字空间)中的 T-specific(T 专用)的 swap。(例如,如果 T 是 namespace WidgetStuff 中的 Widget,编译器会利用 argument-dependent lookup(参数依赖查找)找到 WidgetStuff 中的 swap。)如果 T-specific(T 专用)swap 不存在,编译器将使用 std 中的 swap,这归功于 using declaration 使 std::swap 在此函数中可见。然而,尽管如此,相对于通用模板,编译器还是更喜欢 T-specific(T 专用)的 std::swap 的 specialization(特化),所以如果 std::swap 针对 T 进行了特化,则特化的版本会被使用。

 

得到正确的 swap 调用是如此地容易。你需要小心的一件事是不要对调用加以限定,因为这将影响 C++ 确定应该调用的函数的方式。例如,如果你这样写对 swap 的调用,

std::swap(obj1, obj2);         // the wrong way to call swap

这将强制编译器只考虑 std 中的 swap(包括任何 template specializations(模板特化)),因此排除了获得一个定义在其它地方的更为适用的 T-specific(T 专用)版本的可能性。一些被误导的程序员就是用这种方法限定对 swap 的调用,这也就是为你的 classes 完全地特化 std::swap 很重要的原因:它使得以这种被误导的方式写出的代码可以用到 type-specific(类型专用)的 swap 实现。(这样的代码还存在于现在的一些标准库实现中,所以它将有利于你帮助这样的代码尽可能高效地工作。)

 

到此为止,我们讨论了缺省的 swapmember(成员)swaps,non-member(非成员)swaps,std::swap 的 specializations(特化),以及对 swap 的调用,所以让我们总结一下目前的状况。

 

首先,如果 swap 的缺省实现为你的 class(类)或 class template(类模板)提供了可接受的性能,你不需要做任何事。试图交换你的类型的 objects 的任何人都会得到缺省版本的支持,而且能工作得很好。

 

第二,如果 swap 的缺省实现效率不足(这几乎总是意味着你的 class(类)或 template(模板)使用了 pimpl idiom 的某种变种),就按照以下步骤来做:

提供一个能高效地交换你的类型的两个 objects 的值的 public(公有)的 swap member function(成员函数)。这个函数应该永远不会抛出 exception(异常)。

在你的 class(类)或 template(模板)所在的同一个 namespace(名字空间)中提供一个 non-member(非成员)的 swap。用它调用你的 swap member function(成员函数)。

如果你写了一个 class(类)(不是 class template(类模板)),就为你的 class(类)特化 std::swap。让它也调用你的 swap member function(成员函数)。

最后,如果你调用 swap,请确保包含一个 using declaration 使 std::swap 在你的函数中可见,然后在调用 swap 时不使用任何 namespace(名字空间)限定。

 

唯一没有解决的问题就是——绝不要让 swap 的 member(成员)版本抛出 exceptions(异常)。这是因为 swap 的非常重要的应用之一是为 classes(类)(以及 class templates(类模板))提供强大的 exception-safety(异常安全)保证。这一强制约束仅仅应用在 member(成员)版本上!它不能够应用在 non-member(非成员)版本上,因为 swap 的缺省版本基于 copy construction(拷贝构造)和 copy assignment(拷贝赋值),而在通常情况下,这两个函数都允许抛出异常。如果你写了一个 swap 的自定义版本,那么,典型情况下你不仅要提供一个更有效率的交换值的方法,你还要保证这个方法不会抛出异常。作为一个一般规则,这两个 swap 的特性将紧密地结合在一起,因为高效的 swaps 几乎总是基于 built-in types(内建类型)(诸如在 pimpl idiom 之下的指针)的操作,而对 built-in types(内建类型)的操作绝不会抛出异常。

 

Things to Remember

如果 std::swap 对于你的类型来说是低效的,请提供一个 swap member function(成员函数)。并确保你的 swap 不会抛出异常。

 

如果你提供一个 member(成员)swap,请同时提供一个调用 member(成员)的 non-member(非成员)swap。对于 classes(类)(非 templates(模板)),还要特化 std::swap

 

调用 swap 时,请为 std::swap 使用一个 using declaration ,然后在调用 swap 时不使用任何 namespace(名字空间)限定。

 

user-defined types(用户定义类型)完全地特化 std 模板没有什么问题,但是绝不要试图往 std 中加入任何全新的东西。

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值