跳转到内容

更多 C++ 惯用法/非抛出交换

来自维基教科书,开放世界中的开放书籍

非抛出交换

[编辑 | 编辑源代码]
  • 实现一个异常安全的、高效的交换操作。
  • 为其提供统一的接口以促进泛型编程。

也称为

[编辑 | 编辑源代码]
  • 异常安全的交换

交换的典型实现如下所示

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

这存在两个问题

性能
由于为中间临时对象获取和释放资源,交换两个大型、复杂且类型相同的对象效率低下。
异常安全性
如果资源不可用,此泛型交换实现可能会抛出异常。(这种行为没有意义,因为实际上不应该请求任何新的资源。)因此,此实现不能用于复制并交换 惯用法。

解决方案和示例代码

[编辑 | 编辑源代码]

非抛出交换惯用法使用句柄主体 惯用法来实现所需的效果。正在考虑的抽象被拆分为两个实现类。一个是句柄,另一个是主体。句柄保存指向主体对象的指针。交换被实现为指针的简单交换,保证不会抛出异常,并且非常高效,因为不会获取或释放任何新资源。

namespace Orange {
class String 
{
    char * str;
  public:
    void swap (String &s) // noexcept
    {
      std::swap (this->str, s.str);
    }
};
}

尽管可以实现高效且异常安全的交换函数(如上所示)作为成员函数,但非抛出交换惯用法更进一步,以实现简单性、一致性和促进泛型编程。应该在 std 命名空间以及类的命名空间中添加 std::swap 的显式特化。

namespace Orange { // namespace of String
  void swap (String & s1, String & s2) // noexcept
  {
    s1.swap (s2);
  }
}
namespace std {
  template <>
  void swap (Orange::String & s1, Orange::String & s2) // noexcept
  {
    s1.swap (s2);
  }
}

在两个地方添加它将处理两种不同的常见交换使用方式(1)非限定交换(2)完全限定交换(例如,std::swap)。当使用非限定交换时,使用 Koenig 查找查找正确的交换(如果已经定义)。如果使用完全限定交换,则会抑制 Koenig 查找,而是使用 std 命名空间中的交换。这是一种非常常见的做法。这里剩余的讨论只使用完全限定的交换。它提供了一种统一的外观和感觉,因为 C++ 程序员通常以惯用的方式使用交换函数,将其完全限定为std::,如下所示。

template <class T>
void zoo (T t1, T t2) {
//...
int i1, i2;
std::swap(i1,i2); // note uniformity
std::swap(t1,t2); // Ditto here
}

在这种情况下,当 zoo 使用前面定义的 String 类实例化时,会选择正确的、有效的交换实现。否则,将实例化默认的 std::swap 函数模板——完全违背了定义成员交换和命名空间范围交换函数的目的。在泛型编程中,这种在 std 命名空间中定义交换的显式特化的惯用法特别有用。

使用非抛出交换惯用法的这种统一性会导致其级联使用,如以下示例所示。

class UserDefined 
{
    String str;
  public:
    void swap (UserDefined & u) // noexcept
    { 
      std::swap (str, u.str); 
    }
};
namespace std
{
  // Full specializations of the templates in std namespace can be added in std namespace.
  template <>
  void swap (UserDefined & u1, UserDefined & u2) // noexcept
  {
    u1.swap (u2);
  }
}
class Myclass
{
    UserDefined u;
    char * name;
  public:
    void swap (Myclass & m) // noexcept
    {
      std::swap (u, m.u);       // cascading use of the idiom due to uniformity
      std::swap (name, m.name); // Ditto here
    }   
}
namespace std
{
   // Full specializations of the templates in std namespace can be added in std namespace.
   template <> 
   void swap (Myclass & m1, Myclass & m2) // noexcept
   {
     m1.swap (m2);
   }
};

因此,为适合异常安全、高效交换实现的类型定义 std::swap 的特化是一个好主意。当前的 C++ 标准不允许我们在 std 命名空间中添加新的模板,但允许我们对该命名空间中的模板(例如 std::swap)进行特化,并将它们添加回其中。

注意事项

[编辑 | 编辑源代码]

对模板类(例如,Matrix<T>)使用非抛出交换惯用法可能会是一个微妙的问题。根据 C++98 标准,只有 std::swap 的完全特化才允许在 std 命名空间中为用户定义的类型定义。语言不允许部分特化或函数重载。尝试对模板类(例如,Matrix<T>)实现类似的效果会导致 std 命名空间中 std::swap 的重载,从技术上讲这是未定义的行为。正如 comp.lang.c++.moderated 新闻组中一些人在一段异常长的讨论线程中指出的那样,这并不一定是理想的状态。[1] 有两种可能的解决方案,两者都不完美,可以解决此问题

  1. 符合标准的解决方案。利用 Koenig 查找,在与正在交换的类相同的命名空间中定义一个重载的交换函数模板。并非所有编译器都可能正确支持它,但此解决方案符合标准。[2]
  2. Fingers-crossed 解决方案。部分特化 std::swap 并忽略从技术上讲这是未定义的行为,希望不会发生任何事情,并等待下一个语言标准中的修复。

已知用途

[编辑 | 编辑源代码]

所有 boost 智能指针(例如,boost::shared_ptr)

[编辑 | 编辑源代码]
  1. "命名空间问题,专门交换". comp.lang.c++.moderated (Usenet 新闻组). 2000 年 3 月 12 日。
  2. Sutter,HerbAlexandrescu,Andrei (2004年10月25日). C++ 编码规范:101 条规则、指南和最佳实践. C++ 深入浅出. Addison Wesley Professional. ISBN 0-321-11358-6. 项目 66:不要专门化函数模板。
华夏公益教科书