简体   繁体   English

为什么有些人使用交换进行移动任务?

[英]Why do some people use swap for move assignments?

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?为什么不直接将两个 __u 成员分配给 *this 呢? 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.交换是否暗示 __u 被分配了 *this 成员,只是后来分配了 0 和 false ......在这种情况下,交换正在做不必要的工作。 What am I missing?我错过了什么? (the unique_lock::swap just does an std::swap on each member) (unique_lock::swap 只是对每个成员执行 std::swap )

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).然后某个聪明人(我不记得是谁)向我指出,在分配之前破坏 lhs 的副作用可能很重要(例如您的示例中的 unlock() )。 So I stopped using swap for move assignment.所以我停止使用交换进行移动分配。 But the history of using swap is still there and lingers on.但是使用交换的历史仍然存在,并且还在继续。

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.如果基地和成员的移动分配没有使 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.在某些类中,将移动构造函数实现为 default+swap 正是正确的答案。 The class may be big and complicated. class 可能又大又复杂。 The A(A&&) = default; A(A&&) = default; may do the wrong thing.可能做错事。 I think it is important to consider all of your choices for each class.我认为重要的是要考虑每个 class 的所有选择。

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. A. 这个 class 相当简单。 It has two data members:它有两个数据成员:

 mutex_type* __m_; bool __owns_;

B. This class is in a general purpose library, to be used by an unknown number of clients. B. 此 class 位于通用库中,供未知数量的客户端使用。 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.我们不知道我们的客户是否会在性能关键代码中使用这个 class。 So we have to assume they are.所以我们必须假设它们是。

C. C。 The move constructor for this class is going to consist of a small number of loads and stores, no matter what.无论如何,此 class 的移动构造函数将包含少量加载和存储。 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.例如,如果您用 4 个商店做某事,而其他人只用 2 个商店做同样的事情,那么您的两个实现都非常快。 But their's is twice as fast as yours.但是他们的速度是你的两倍 That difference could be critical in some client's tight loop.在某些客户的紧密循环中,这种差异可能至关重要。

First lets count loads and stores in the default constructor, and in the member swap function:首先让我们在默认构造函数和成员交换 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.对于libc++ ,我选择了更快的解决方案。 My rationale is that for this class, I better get it right no matter what;我的理由是,对于这个 class,无论如何我都最好把它做好; the class is simple enough that my chances of getting it right are high; class 很简单,我做对的机会很高; and my clients are going to value performance.我的客户会重视绩效。 I might well come to another conclusion for a different class in a different context.在不同的上下文中,对于不同的 class,我很可能会得出另一个结论。

It's about exception safety.这是关于异常安全的。 Since __u is already constructed when the operator is called, we know there's no exception, and swap doesn'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_ptr takes 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在赋值中采用明确的右值引用,因为它不允许复制构造/赋值,因此它不是此设计原则的最佳示例。)

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,举个例子,

 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:在未优化的移动 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.在某些情况下,由于“聪明”的值语义,交换比分配更好,例如,如果 class 中的成员之一是 std::shared_ptr。 No reason a move constructor should mess with the atomic refcounter.移动构造函数没有理由与原子引用计数器混淆。

I will answer the question from header: "Why do some people use swap for move assignments?".我将回答来自 header 的问题:“为什么有些人使用交换进行移动分配?”。

The primary reason to use swap is providing noexcept move assignment .使用swap的主要原因是提供 noexcept move assignment

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 !但总的来说,销毁/释放 function 可能会失败并抛出异常

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

#2 is perfectly noexcept! #2完全没有例外!

Yes, close() call can be "delayed" in case #2 .是的,在#2的情况下, close()调用可以“延迟”。 But!但! If we want strict error checking we must use explicit close() call, not destructor.如果我们想要严格的错误检查,我们必须使用显式close()调用,而不是析构函数。 Destructor releases resource only in "emergency" situations, where exeption can't be thrown anyway.析构函数仅在“紧急”情况下释放资源,无论如何都不能抛出异常。

PS See also discussion here in comments PS 另请参阅评论中讨论

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM