C++ 使交换更快、更易于使用且异常安全

C++ 使交换更快、更易于使用且异常安全,c++,swap,move-semantics,c++11,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; } 不必再摆弄swap,程序员已经减轻了很多负担。 目前的编译器还没有自动生成移动构造函数和移动赋值操作符,但据我所知,这将发生变化。剩下的唯一问题是异常安全,因为一般来说,移动操作允许抛出,这会打开一整罐蠕虫。“从对象移动的对象的状态到

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

template <typename T>
void swap(T& a, T& b)
{
    T c(a);
    a = b;
    b = c;
}
不必再摆弄
swap
,程序员已经减轻了很多负担。 目前的编译器还没有自动生成移动构造函数和移动赋值操作符,但据我所知,这将发生变化。剩下的唯一问题是异常安全,因为一般来说,移动操作允许抛出,这会打开一整罐蠕虫。“从对象移动的对象的状态到底是什么?”这一问题使事情进一步复杂化

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

那么,为什么不简单地编写一个
swap
模板来实现这一点:交换对象表示

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

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


请让我知道您对这种交换方法的看法。它在实践中会起作用吗?你会用它吗?您能确定这会在哪里中断的库类型吗?你看到其他问题了吗?讨论

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

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


常用的习惯用法是提供void Foo::swap(Foo&other)方法和std::swap的专门化。请注意,这不适用于类模板

更好的习惯用法是非成员交换,要求用户调用swap unqualified,因此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
  }
};
struct NonTemplate{};
无效掉期(非模板&,非模板&);
模板
结构模板{
朋友无效掉期(模板a、模板b){
使用std::swap;
#定义S(N)互换(a.N,b.N);
S(每个)
S(数据)
S(成员)
#未定义的
}
};
关键是使用std::swap的声明作为回退。模板交换的友好性有助于简化定义;非模板交换可能也是一个好方法,但这是一个实现细节

那么,为什么不简单地编写一个
swap
模板来实现这一点:交换对象表示*

一个对象一旦被构造,当你复制它所驻留的字节时,有很多种方式可以使它中断。事实上,我们可能会遇到无数这样做不正确的案例,尽管在实践中,这可能在98%的案例中都有效

这是因为所有这些问题的根本问题是,除了C中,C++中的<>强>不能把对象当作原始字节< /强>对待。这就是为什么我们有构建和销毁:将原始存储转换为对象,将对象重新转换为原始存储。一旦构造函数运行,对象所在的内存就不仅仅是原始存储。如果你把它当作不是,你会打破一些类型

但是,从本质上讲,移动对象的性能不应该比您的想法差太多,因为一旦您开始递归地内联调用
std::move()
,您通常最终会到达移动内置的位置。(如果某些类型的移动还有很多,你最好不要自己摆弄它们的内存!)当然,整体移动内存通常比单次移动快(而且编译器不太可能发现它可以将单个移动优化为一个包罗万象的
std::memcpy()
),但这就是我们为不透明对象提供的抽象所付出的代价。而且它很小,特别是当你把它和我们以前做的复制品比较时


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

这将中断具有指向其自身成员的指针的类实例。例如:

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会指向哪里?显然是到老地方去了。这将导致非常有趣的bug,其中每个实例实际使用另一个实例的缓冲区。

如果有人使用多态类型,您的
交换版本将造成严重破坏

考虑:

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包含
派生的
类型的vtable,所以
派生的::vfunc
是在一个不是
派生的
类型的对象上调用的

正常的
std::swap
只交换
Base
的数据成员,因此这对于
std::swap

我认为这是个小问题,因为 这类物体可能应该 未在中提供复制操作 第一位

很简单,这是一大堆错误。通知观察者的类和不应复制的类是完全不相关的。共享ptr怎么样?它显然应该是可复制的,但它也会通知观察者引用计数。在这种情况下,交换后的引用计数是相同的,但这肯定不是所有类型都是如此,如果涉及多线程,尤其是在常规副本而不是交换等情况下,这是不正确的。对于可以移动或删除的类,这尤其是错误的
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
  }
};
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);
}
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
//...