使交换更快,更易于使用和异常安全

时间:2011-02-02 14:00:00

标签: c++ swap move-semantics c++11

昨晚我睡不着觉,开始考虑std::swap。这是熟悉的C ++ 98版本:

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

如果用户定义的类Foo使用外部资源,则效率低下。常见的习惯用法是提供方法void Foo::swap(Foo& other)std::swap<Foo>的特化。请注意,这不适用于类 templates ,因为您无法部分地专门化函数模板,并且重载std命名空间中的名称是非法的。解决方案是在一个人自己的命名空间中编写模板函数,并依赖于参数依赖查找来查找它。这主要取决于客户遵循“using std::swap惯用语”而不是直接调用std::swap。非常脆弱。

在C ++ 0x中,如果Foo具有用户定义的移动构造函数和移动赋值运算符,则提供自定义swap方法和std::swap<Foo>特化几乎没有性能好处,因为std::swap的C ++ 0x版本使用有效的移动而不是副本:

#include <utility>

template <typename T>
void swap(T& a, T& b)
{
    T c(std::move(a));
    a = std::move(b);
    b = std::move(c);
}

不再需要摆弄swap已经给程序员带来了很多负担。 当前的编译器不会自动生成移动构造函数和移动赋值运算符,但据我所知,这将改变。剩下的唯一问题是异常安全,因为一般来说,允许移动操作,这会打开一大堆蠕虫。问题是“移动对象的状态究竟是什么?”使事情进一步复杂化。

然后我在想,如果一切顺利的话,C ++ 0x中std::swap的语义究竟是什么?交换之前和之后的对象状态是什么?通常,通过移动操作进行交换不会触及外部资源,只会触及“平面”对象表示本身。

那么为什么不简单地编写一个swap模板来完成那个:交换对象表示

#include <cstring>

template <typename T>
void swap(T& a, T& b)
{
    unsigned char c[sizeof(T)];

    memcpy( c, &a, sizeof(T));
    memcpy(&a, &b, sizeof(T));
    memcpy(&b,  c, sizeof(T));
}

这是有效的:它只是通过原始内存爆炸。它不需要用户的任何干预:不必定义特殊的交换方法或移动操作。这意味着它甚至可以在C ++ 98中工作(没有右值引用,请注意)。但更重要的是,我们现在可以忘记异常安全问题,因为memcpy永远不会抛出。

我可以看到这种方法存在两个潜在的问题:

首先,并非所有对象都要交换。如果类设计者隐藏了复制构造函数或复制赋值运算符,则尝试交换该类的对象应该在编译时失败。我们可以简单地引入一些死代码来检查复制和赋值是否合法:

template <typename T>
void swap(T& a, T& b)
{
    if (false)    // dead code, never executed
    {
        T c(a);   // copy-constructible?
        a = b;    // assignable?
    }

    unsigned char c[sizeof(T)];

    std::memcpy( c, &a, sizeof(T));
    std::memcpy(&a, &b, sizeof(T));
    std::memcpy(&b,  c, sizeof(T));
}

任何体面的编译器都可以轻而易举地摆脱死代码。 (可能有更好的方法来检查“交换一致性”,但这不是重点。重要的是它是可能的)。

其次,某些类型可能会在复制构造函数和复制赋值运算符中执行“异常”操作。例如,他们可能会通知观察者他们的变化。我认为这是一个小问题,因为这类对象可能不应该首先提供复制操作。

请让我知道您对这种交换方法的看法。它会在实践中起作用吗?你会用吗?你能识别出会破坏的库类型吗?你看到其他问题吗?讨论!

5 个答案:

答案 0 :(得分:20)

  

那么为什么不简单地编写一个swap模板来完成那个: swap 对象表示*?

当你复制它所在的字节时,有很多种方法可以使一个对象一旦被构造出来。事实上,人们可能会想出一些看似无穷无尽的案例 做正确的事 - 即使在实践中它可能在98%的情况下都有效。

这是因为所有这一切的根本问题在于,除了在C中,在C ++中我们 一定不能将对象视为纯粹的原始字节 。毕竟,这就是为什么我们需要构建和销毁:将原始存储转换为对象和对象回原始存储。一旦构造函数运行,对象所在的内存不仅仅是原始存储。如果你把它视为不是,那么你会破坏某些类型。

然而,基本上,移动对象的性能不应该比你的想法差得多,因为一旦你开始递归地内联调用std::move(),你通常最终会到达 内置的地方-ins被移动 。 (如果对于某些类型有更多动作,你最好不要动摇那些你自己的记忆!)当然,整体移动内存通常比单动更快(并且编译器不太可能发现它可以优化个人移动到一个无所不包的std::memcpy()),但这是我们为不透明对象提供给我们的抽象付出的代价。它非常小,特别是当你将它与我们过去的复制进行比较时。

但是,对于 聚合类型 ,您可以使用swap()优化std::memcpy()

答案 1 :(得分:20)

这将破坏具有指向其自己成员的指针的类实例。例如:

class SomeClassWithBuffer {
  private:
    enum {
      BUFSIZE = 4096,
    };
    char buffer[BUFSIZE];
    char *currentPos; // meant to point to the current position in the buffer
  public:
    SomeClassWithBuffer();
    SomeClassWithBuffer(const SomeClassWithBuffer &that);
};

SomeClassWithBuffer::SomeClassWithBuffer():
  currentPos(buffer)
{
}

SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that)
{
  memcpy(buffer, that.buffer, BUFSIZE);
  currentPos = buffer + (that.currentPos - that.buffer);
}

现在,如果你只是做memcpy(),currentPos指向哪里?到了旧位置,显然。这将导致非常有趣的错误,每个实例实际上使用另一个缓冲区。

答案 2 :(得分:7)

某些类型可以交换但无法复制。独特的智能指针可能是最好的例子。检查可复制性和可分配性是错误的。

如果T不是POD类型,则使用memcpy进行复制/移动是未定义的行为。


  

常见的习惯用法是提供一个方法void Foo :: swap(Foo&amp; other)和std :: swap&lt; Foo&gt;的特化。请注意,这不适用于类模板,...

更好的习惯用法是非成员交换并要求用户调用swap不合格,因此ADL适用。这也适用于模板:

struct NonTemplate {};
void swap(NonTemplate&, NonTemplate&);

template<class T>
struct Template {
  friend void swap(Template &a, Template &b) {
    using std::swap;
#define S(N) swap(a.N, b.N);
    S(each)
    S(data)
    S(member)
#undef S
  }
};

关键是std :: swap的using声明作为回退。 Template's swap的友谊对于简化定义很有帮助; NonTemplate的交换也可能是朋友,但这是一个实现细节。

答案 3 :(得分:6)

  

我认为这是一个小问题,因为   这类物体可能应该   没有提供复制操作   第一名。

就是说,很简单,一堆错误。通知观察者和不应复制的类的类完全不相关。 shared_ptr怎么样?它显然应该是可复制的,但它显然也会通知观察者 - 引用计数。现在确实,在这种情况下,交换后引用计数是相同的,但对于所有类型来说绝对不是这样,如果涉及多线程,它的尤其不是真的,它不是真的对于可移动或交换但复制的类,尤其错误。

  

因为一般来说,移动操作   被允许扔掉

他们绝对不是。几乎不可能在任何涉及移动的情况下保证强大的异常安全性。标准库的C ++ 0x定义,从内存中,明确指出任何标准容器中可用的任何类型在移动时都不能抛出。

  

这是有效的

这也是错误的。你假设任何对象的移动纯粹是它的成员变量 - 但它可能不是全部。我可能有一个基于实现的缓存,我可能会决定在我的课程中,我不应该移动这个缓存。作为一个实现细节,完全在我的权利范围内,不移动我认为不需要移动的任何成员变量。但是,你想要移动所有这些。

现在,您的示例代码对许多类都有效。但是,对于许多完全合法的类来说,它绝对非常有效,更重要的是,如果操作可以减少到那么,它将编译为无论如何。这打破了完美的好课程,绝对没有任何好处。

答案 4 :(得分:1)

如果有人将其与多态类型一起使用,那么您的swap版本将会造成严重破坏。

考虑:

Base *b_ptr = new Base();    // Base and Derived contain definitions
Base *d_ptr = new Derived(); // of a virtual function called vfunc()
yourmemcpyswap( *b_ptr, *d_ptr );
b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc
d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc
//...

这是错误的,因为现在b包含Derived类型的vtable,因此在Derived::vfunc类型的对象上调用Derived

普通std::swap仅交换Base的数据成员,因此std::swap

就可以了