c++ - 使交换更快、更容易使用和异常安全

标签 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具有用户定义的 move 构造函数和 move 赋值运算符,提供自定义 swap方法和 std::swap<Foo>特化几乎没有性能优势,因为 std::swap 的 C++0x 版本使用有效的 move 而不是复制:

#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已经从程序员身上减轻了很多负担。 当前的编译器还没有自动生成 move 构造函数和 move 赋值运算符,但据我所知,这将改变。剩下的唯一问题是异常安全,因为一般来说, move 操作是允许抛出的,这会打开一大堆蠕虫。问题“移出对象的状态究竟是什么?”让事情变得更加复杂。

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

那么为什么不简单地写一个 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));
}

这是最有效的:它只是通过原始内存爆炸。它不需要用户的任何干预:不需要定义特殊的交换方法或 move 操作。这意味着它甚至可以在 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));
}

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

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

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

最佳答案

So why not simply write a swap template that does exactly that: swap the object representations*?

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

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

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

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

关于c++ - 使交换更快、更容易使用和异常安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4875289/

相关文章:

C++ 为什么在我的类中添加析构函数会使我的类无法 move ?

c++ - 移动速度计算问题

c - C函数在char [] segfaults中交换两个char,尽管不使用字符串文字

java - 如何用 Java 编写一个基本的交换函数

javascript - 点击交换两个元素

c++ - 使用 std::move 来防止复制

c++ - 在 C++ 上获取 radio 组的值

c++ - 推导忽略的参数包

c++ - 在函数周围传递 char[]

未调用 C++11 move 构造函数,首选默认构造函数