【问题标题】:Making swap faster, easier to use and exception-safe使交换更快、更容易使用和异常安全
【发布时间】:2011-06-20 00:06:22
【问题描述】:

昨晚睡不着,开始想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&amp; other) 和一个专门化的std::swap&lt;Foo&gt;。请注意,这不适用于类 templates,因为您不能部分特化函数模板,并且在 std 命名空间中重载名称是非法的。解决方案是在自己的命名空间中编写一个模板函数,并依靠参数依赖查找来找到它。这严重依赖于客户端遵循“using std::swap idiom”而不是直接调用std::swap。很脆。

在 C++0x 中,如果 Foo 具有用户定义的移动构造函数和移动赋值运算符,则提供自定义 swap 方法和 std::swap&lt;Foo&gt; 特化几乎没有性能优势,因为 C+ +0x 版本的std::swap 使用高效的移动而不是复制:

#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));
}

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

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

请告诉我您对这种交换方法的看法。它会在实践中起作用吗?你会用吗?你能确定这会破坏的库类型吗?您是否看到其他问题?讨论!

【问题讨论】:

  • std::swap 的大多数当前用例无论如何都会有更好的使用移动语义的解决方案。
  • 是移动语义和移动构造函数。看到这个,stackoverflow.com/questions/4820643/…
  • @smerlin:但是多态对象真的不应该有复制构造函数或赋值运算符,对吧?
  • @smerlin:实际上,是的 :) C++0x 提供了 std::is_polymorphic 类型特征。

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


【解决方案1】:

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

当你复制它所在的字节时,一个对象一旦被构造,可能会以多种方式破坏它。事实上,人们可能会想出无数种情况这不会做正确的事 - 即使在实践中它可能适用于所有情况的 98%。

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

然而,本质上,移动对象的性能不应该比你的想法差那么多,因为一旦你开始递归内联对 std::move() 的调用,你通常最终会到达 内置函数被移动。 (如果对某些类型有更多的移动,你最好不要自己去摆弄那些内存!)当然,整体移动内存通常比单个移动更快(编译器不太可能发现它可以优化单个动作到一个包罗万象的std::memcpy()),但这就是我们为抽象不透明对象提供给我们的代价。而且它非常小,尤其是当您将其与我们过去进行的复制进行比较时。

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

【讨论】:

  • s/praxis/practice/ ;-) 另外,最好添加一个指向 POD/聚合常见问题解答的链接。
  • @Fred,我的字典说“praxis”是一个完全有效的英文单词。不是吗?英语不是我的母语,但我很好奇。
  • @Sergey:Google 有 27.700.000 个“in practice”搜索结果,但只有 214.000 个“in praxis”搜索结果...
  • 您确定没有将相邻动作合并为一个动作吗?这似乎是一个微不足道的优化。 (我想它可能会因为像虚拟表指针这样的“未移动”数据的存在而被抛弃)
  • 马修:不,我不确定。请注意,我写的是“不太可能”。
【解决方案2】:

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

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 会指向哪里?到老位置,很明显。这将导致非常有趣的错误,即每个实例实际上都使用另一个实例的缓冲区。

【讨论】:

  • 老实说,让 Reader 对象可复制和可分配对我来说似乎是一个设计错误。
  • @Fred,这只是一个抽象的例子。我可能应该将其命名为“SomeClassWithBuffer”,但这无关紧要。
  • @Matthieu,OP 提到了这个问题,所以我没有提到。起初看起来可能还有更多问题。
  • 此外,OP 建议的缓冲区可能未正确对齐 T
  • 赞成:相关琐事:libstdc++ 和 libc++ (libcxx.llvm.org) 中的每个基于节点的 std::container 都具有 Sergey 说明的设计(因此会与 memcpy-swap 中断)。这就是“嵌入式端节点”优化,是基于节点的容器更重要的优化之一。这同时启用了 noexcept 默认构造函数和 noexcept 移动构造函数。恕我直言,没有比这更重要的了。自然地,这些容器可以并且确实创建了它们自己的交换重载。但重点是:谢尔盖的设计并不罕见。
【解决方案3】:

有些类型可以交换但不能复制。唯一的智能指针可能是最好的例子。检查可复制性和可分配性是错误的。

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


常见的用法是提供方法 void Foo::swap(Foo& other) 和 std::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 的交换友好性有助于简化定义; NonTemplate 的交换也可能是一个朋友,但这是一个实现细节。

【讨论】:

    【解决方案4】:

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

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

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

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

    这是最有效的

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

    现在,您的示例代码确实应该适用于很多类。但是,对于许多完全合法的类来说,这绝对是无效的,更重要的是,如果操作可以简化为该操作,它将编译为该操作无论如何。这完全破坏了非常好的课程,绝对没有任何好处。

    【讨论】:

    • +1 指出:“...标准库的 C++0x 定义,从内存中明确指出,在任何标准容器中可用的任何类型在移动时都不得抛出。... 。”
    • 我会为“...添加另一个 +1,更重要的是,无论如何它都会编译为该操作...”
    【解决方案5】:

    如果有人将您的 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 类型的对象上调用了 Derived::vfunc

    普通的std::swap只交换Base的数据成员,所以std::swap可以这样

    【讨论】:

    • 只交换Base 的数据成员会破坏Derived 对象的不变量。这就是为什么对多态对象使用赋值运算符没有多大意义的原因之一。请注意,Bjarne Stroustrup 认为默认情况下为每个用户定义的类提供赋值运算符是一个历史意外。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-13
    • 1970-01-01
    • 2017-07-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多