Effective C++笔记: 设计与声明(四)

来源:互联网 发布:javaweb和javascript 编辑:程序博客网 时间:2024/06/07 12:57

 

Item 25: 考虑写出一个不抛出异常的 swap函数

标准程序库中的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。对一些类型来说,这些复制动作是没有必要的。最主要的就是采用pimpl(pointer to implementation), “以指针指向一个对象,内含真正数据“的这种类型。

如:

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::swapspecialize 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
}

可是,就像我说的,这个函数还不能编译。那是因为它试图访问 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 的特化来调用这些成员函数。

 

stlvectorswap实现:(SGI STL 3.3)

std::swap:

template <class _Tp>

inline void swap(_Tp& __a, _Tp& __b) {

  __STL_REQUIRES(_Tp, _Assignable);

  _Tp __tmp = __a;

  __a = __b;

  __b = __tmp;

}

 

vectorpublic swap(其实就是交换三个_Tp* 指针既可)

  void swap(vector<_Tp, _Alloc>& __x) {

    __STD::swap(_M_start, __x._M_start);

    __STD::swap(_M_finish, __x._M_finish);

    __STD::swap(_M_end_of_storage, __x._M_end_of_storage);

  }

 

特化swap(调用vector类的public  swap成员)

template <class _Tp, class _Alloc>

inline void swap(vector<_Tp, _Alloc>& __x, vector<_Tp, _Alloc>& __y)

{

  __x.swap(__y);

}

 

!!给自己的模板类添加swap:

假设 Widget WidgetImpl 是类模板,而不是类,或许因此我们可以参数化存储在 WidgetImpl 中的数据类型:(其实这就和stl vector的行为一样)

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),但不允许函数模板这样做。这样的代码不能编译.

当我们想要部分特化一个函数模板时,通常做法是简单地增加一个重载。看起来就像这样:(pure like std::vector的实现办法)

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,只是不再将那个非成员函数声明为 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);
  }
}

然后,假设你写了这样一个函数模板来交换两个对象的值:

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

当编译器看到这个 swap 调用,他会寻找正确的 swap 版本来调用。C++ 的名字查找规则确保能找到在全局 namespace 或者与 T 同一个 namespace 中的 T 专用的 swap。(例如,如果 T namespace WidgetStuff 中的 Widget,编译器会利用参数依赖查找(argument-dependent lookup)找到 WidgetStuff 中的 swap。)如果 T 专用 swap 不存在,编译器将使用 std 中的 swap,这归功于此函数中的 using declaration 使 std::swap 在此可见。尽管如此,相对于通用模板,编译器还是更喜欢 T 专用的 std::swap 的特化,所以如果 std::swap T 进行了特化,则特化的版本会被使用。

采用如下的版本可以保证以上的调用顺序:

如果 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 的缺省实现效率不足(这几乎总是意味着你的类或模板使用了某种 pimpl idiom 的变种),就按照以下步骤来做:

1.   提供一个能高效地交换你的类型的两个对象的值的 public swap 成员函数。出于我过一会儿就要解释的动机,这个函数应该永远不会抛出异常

2.   在你的类或模板所在的同一个 namespace 中提供一个非成员的 swap。用它调用你的 swap 成员函数。

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

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

 

成员版swap绝不能抛出异常!!(后继条款29对此说明)

 

原创粉丝点击