C++ 为什么有些人使用交换进行移动分配?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6687388/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 20:32:14  来源:igfitidea点击:

Why do some people use swap for move assignments?

c++c++11rvalue-referencemove-semanticscopy-and-swap

提问by Display Name

For example, stdlibc++ has the following:

例如,stdlibc++ 具有以下内容:

unique_lock& operator=(unique_lock&& __u)
{
    if(_M_owns)
        unlock();
    unique_lock(std::move(__u)).swap(*this);
    __u._M_device = 0;
    __u._M_owns = false;
    return *this;
}

Why not just assign the two __u members to *this directly? Doesn't the swap imply that __u is assigned the *this members, only to later have then assigned 0 and false... in which case the swap is doing unnecessary work. What am I missing? (the unique_lock::swap just does an std::swap on each member)

为什么不直接将两个 __u 成员分配给 *this 呢?交换是否意味着 __u 被分配了 *this 成员,只是后来分配了 0 和 false ......在这种情况下,交换正在做不必要的工作。我错过了什么?( unique_lock::swap 只是对每个成员执行 std::swap )

回答by Howard Hinnant

It's my fault. (half-kidding, half-not).

我的错。(半开玩笑,半不是)。

When I first showed example implementations of move assignment operators, I just used swap. Then some smart guy (I can't remember who) pointed out to me that the side effects of destructing the lhs prior to the assignment might be important (such as the unlock() in your example). So I stopped using swap for move assignment. But the history of using swap is still there and lingers on.

当我第一次展示移动赋值运算符的示例实现时,我只是使用了交换。然后一些聪明人(我不记得是谁)向我指出在分配之前破坏 lhs 的副作用可能很重要(例如你的例子中的 unlock() )。所以我停止使用交换进行移动分配。但是使用交换的历史仍然存在并且持续存在。

There's no reason to use swap in this example. It is less efficient than what you suggest. Indeed, in libc++, I do exactly what you suggest:

在这个例子中没有理由使用交换。它比您建议的效率低。事实上,在libc++ 中,我完全按照你的建议去做:

unique_lock& operator=(unique_lock&& __u)
    {
        if (__owns_)
            __m_->unlock();
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

In general a move assignment operator should:

通常,移动赋值运算符应该:

  1. Destroy visible resources (though maybe save implementation detail resources).
  2. Move assign all bases and members.
  3. If the move assignment of bases and members didn't make the rhs resource-less, then make it so.
  1. 销毁可见资源(尽管可能会保存实现细节资源)。
  2. 移动分配所有基地和成员。
  3. 如果基地和成员的移动分配没有使 rhs 资源减少,那么就这样做。

Like so:

像这样:

unique_lock& operator=(unique_lock&& __u)
    {
        // 1. Destroy visible resources
        if (__owns_)
            __m_->unlock();
        // 2. Move assign all bases and members.
        __m_ = __u.__m_;
        __owns_ = __u.__owns_;
        // 3. If the move assignment of bases and members didn't,
        //           make the rhs resource-less, then make it so.
        __u.__m_ = nullptr;
        __u.__owns_ = false;
        return *this;
    }

Update

更新

In comments there's a followup question about how to handle move constructors. I started to answer there (in comments), but formatting and length constraints make it difficult to create a clear response. Thus I'm putting my response here.

在评论中有一个关于如何处理移动构造函数的后续问题。我开始在那里回答(在评论中),但格式和长度限制使得很难做出明确的回应。因此,我将我的回应放在这里。

The question is: What's the best pattern for creating a move constructor? Delegate to the default constructor and then swap? This has the advantage of reducing code duplication.

问题是:创建移动构造函数的最佳模式是什么?委托给默认构造函数然后交换?这具有减少代码重复的优点。

My response is: I think the most important take-away is that programmers should be leery of following patterns without thought. There may be some classes where implementing a move constructor as default+swap is exactly the right answer. The class may be big and complicated. The A(A&&) = default;may do the wrong thing. I think it is important to consider all of your choices for each class.

我的回答是:我认为最重要的一点是程序员应该警惕不加思考地遵循模式。可能有一些类将移动构造函数作为默认值+交换实现是完全正确的答案。这个类可能很大而且很复杂。该A(A&&) = default;会做错事。我认为重要的是要考虑您对每个课程的所有选择。

Let's take a look at the OP's example in detail: std::unique_lock(unique_lock&&).

让我们详细看看OP的例子: std::unique_lock(unique_lock&&)

Observations:

观察:

A. This class is fairly simple. It has two data members:

A. 这个类相当简单。它有两个数据成员:

mutex_type* __m_;
bool __owns_;
mutex_type* __m_;
bool __owns_;

B. This class is in a general purpose library, to be used by an unknown number of clients. In such a situation, performance concerns are a high priority. We don't know if our clients are going to be using this class in performance critical code or not. So we have to assume they are.

B. 此类位于通用库中,供未知数量的客户端使用。在这种情况下,性能问题是重中之重。我们不知道我们的客户是否会在性能关键代码中使用这个类。所以我们必须假设它们是。

C. The move constructor for this class is going to consist of a small number of loads and stores, no matter what. So a good way to look at the performance is to count loads and stores. For example if you do something with 4 stores, and somebody else does the same thing with only 2 stores, both of your implementations are very fast. But their's is twiceas fast as yours! That difference could be critical in some client's tight loop.

C. 无论如何,此类的移动构造函数将由少量加载和存储组成。所以查看性能的一个好方法是计算加载和存储。例如,如果您用 4 个商店做某事,而其他人只用 2 个商店做同样的事情,那么您的两个实现都非常快。但他们的速度是你的两倍!这种差异在某些客户的紧密循环中可能至关重要。

First lets count loads and stores in the default constructor, and in the member swap function:

首先让我们计算默认构造函数和成员交换函数中的加载和存储:

// 2 stores
unique_lock()
    : __m_(nullptr),
      __owns_(false)
{
}

// 4 stores, 4 loads
void swap(unique_lock& __u)
{
    std::swap(__m_, __u.__m_);
    std::swap(__owns_, __u.__owns_);
}

Now lets implement the move constructor two ways:

现在让我们通过两种方式实现移动构造函数:

// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
    : __m_(__u.__m_),
      __owns_(__u.__owns_)
{
    __u.__m_ = nullptr;
    __u.__owns_ = false;
}

// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
    : unique_lock()
{
    swap(__u);
}

The first way looks much more complicated than the second. And the source code is larger, and somewhat duplicating code we might have already written elsewhere (say in the move assignment operator). That means there's more chances for bugs.

第一种方式看起来比第二种方式复杂得多。并且源代码更大,并且有些重复我们可能已经在其他地方编写的代码(例如在移动赋值运算符中)。这意味着有更多的机会出现错误。

The second way is simpler and reuses code we've already written. Thus less chance of bugs.

第二种方法更简单,可以重用我们已经编写的代码。从而减少错误的机会。

The first way is faster. If the cost of loads and stores is approximately the same, perhaps 66% faster!

第一种方式更快。如果加载和存储的成本大致相同,则可能快 66%!

This is a classic engineering tradeoff. There is no free lunch. And engineers are never relieved of the burden of having to make decisions about tradeoffs. The minute one does, planes start falling out of the air and nuclear plants start melting down.

这是一个经典的工程权衡。天下没有免费的午餐。工程师永远无法摆脱必须做出权衡决定的负担。一分钟,飞机开始从空中坠落,核电站开始融化。

For libc++, I chose the faster solution. My rationale is that for this class, I better get it right no matter what; the class is simple enough that my chances of getting it right are high; and my clients are going to value performance. I might well come to another conclusion for a different class in a different context.

对于libc++,我选择了更快的解决方案。我的理由是,对于这门课,无论如何我最好做对;这门课很简单,我做对的机会很高;我的客户将重视绩效。对于不同背景下的不同班级,我很可能会得出另一个结论。

回答by Kerrek SB

It's about exception safety. Since __uis already constructed when the operator is called, we know there's no exception, and swapdoesn't throw.

这是关于异常安全的。由于__u在调用操作符时已经构造好了,我们知道没有异常,swap也不会抛出。

If you did the member assignments manually, you'd risk that each of those might throw an exception, and then you'd have to deal with having partially move-assigned something but having to bail out.

如果您手动进行成员分配,您将面临每个成员都可能抛出异常的风险,然后您必须处理部分移动分配的内容但不得不退出。

Maybe in this trivial example this doesn't show, but it's a general design principle:

也许在这个简单的例子中没有显示,但这是一个通用的设计原则:

  • Copy-assign by copy-construct and swap.
  • Move-assign by move-construct and swap.
  • Write +in terms of construct and +=, etc.
  • 通过复制构造和交换进行复制分配。
  • 通过移动构造和交换移动分配。
  • +用构造和+=等来写。

Basically, you try to minimize the amount of "real" code and try to express as many other features in terms of the core features as you can.

基本上,您会尽量减少“真实”代码的数量,并尝试根据核心功能表达尽可能多的其他功能。

(The unique_ptrtakes an explicit rvalue reference in the assignment because it does not permit copy construction/assignment, so it's not the best example of this design principle.)

unique_ptr在赋值中采用显式右值引用,因为它不允许复制构造/赋值,所以它不是这个设计原则的最好例子。)

回答by yonil

Another thing to consider regarding the trade-off:

关于权衡需要考虑的另一件事:

The default-construct + swap implementation may appear slower, but -sometimes- data flow analysis in the compiler can eliminate some pointless assignments and end up very similar to handwritten code. This works only for types without "clever" value semantics. As an example,

default-construct + swap 实现可能看起来更慢,但有时编译器中的数据流分析可以消除一些无意义的赋值,最终与手写代码非常相似。这仅适用于没有“聪明”值语义的类型。举个例子,

 struct Dummy
 {
     Dummy(): x(0), y(0) {} // suppose we require default 0 on these
     Dummy(Dummy&& other): x(0), y(0)
     {
         swap(other);             
     }

     void swap(Dummy& other)
     {
         std::swap(x, other.x);
         std::swap(y, other.y);
         text.swap(other.text);
     }

     int x, y;
     std::string text;
 }

generated code in move ctor without optimization:

未经优化在 move ctor 中生成的代码:

 <inline std::string() default ctor>
 x = 0;
 y = 0;
 temp = x;
 x = other.x;
 other.x = temp;
 temp = y;
 y = other.y;
 other.y = temp;
 <inline impl of text.swap(other.text)>

This looks awful, but data flow analysis can determine it is equivalent to the code:

这看起来很糟糕,但数据流分析可以确定它等价于代码:

 x = other.x;
 other.x = 0;
 y = other.y;
 other.y = 0;
 <overwrite this->text with other.text, set other.text to default>

Maybe in practice compilers won't always produce the optimal version. Might want to experiment with it and take a glance at the assembly.

也许在实践中编译器不会总是产生最佳版本。可能想尝试一下并看一眼程序集。

There are also cases when swapping is better than assigning because of "clever" value semantics, for example if one of the members in the class is a std::shared_ptr. No reason a move constructor should mess with the atomic refcounter.

也有交换比赋值更好的情况,因为“聪明”的值语义,例如,如果类中的一个成员是 std::shared_ptr。没有理由移动构造函数应该与原子引用计数器混淆。

回答by Victor Dyachenko

I will answer the question from header: "Why do some people use swap for move assignments?".

我将回答标题中的问题:“为什么有些人使用交换进行移动分配?”。

The primary reason to use swapis providing noexcept move assignment.

使用的主要原因swap提供 noexcept 移动分配

From Howard Hinnant's comment:

来自 Howard Hinnant 的评论:

In general a move assignment operator should:
1. Destroy visible resources (though maybe save implementation detail resources).

通常,移动赋值运算符应该:
1. 销毁可见资源(尽管可能会保存实现细节资源)。

But in general destroy/release function can fail and throw exception!

但一般来说,销毁/释放功能可能会失败并抛出异常

Here is an example:

下面是一个例子:

class unix_fd
{
    int fd;
public:
    explicit unix_fd(int f = -1) : fd(f) {}
    ~unix_fd()
    {
        if(fd == -1) return;
        if(::close(fd)) /* !!! call is failed! But we can't throw from destructor so just silently ignore....*/;
    }

    void close() // Our release-function
    {
        if(::close(fd)) throw system_error_with_errno_code;
    }
};

Now let's compare two implementaions of move-assignment:

现在让我们比较一下移动赋值的两种实现:

// #1
void unix_fd::operator=(unix_fd &&o) // Can't be noexcept
{
    if(&o != this)
    {
        close(); // !!! Can throw here
        fd = o.fd;
        o.fd = -1;
    }
    return *this;
}

and

// #2
void unix_fd::operator=(unix_fd &&o) noexcept
{
    std::swap(fd, o.fd);
    return *this;
}

#2is perfectly noexcept!

#2完全没有例外!

Yes, close()call can be "delayed" in case #2. But! If we want strict error checking we must use explicit close()call, not destructor. Destructor releases resource only in "emergency" situations, where exeption can't be thrown anyway.

是的,close()呼叫可以“延迟”以防万一#2。但!如果我们想要严格的错误检查,我们必须使用显式close()调用,而不是析构函数。析构函数仅在“紧急”情况下释放资源,无论如何都不能抛出异常。

P.S. See also discussion herein comments

PS参见讨论这里的评论