c++ - 为什么有些人使用交换进行 move 分配?

标签 c++ c++11 rvalue-reference move-semantics copy-and-swap

例如,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;
}

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

最佳答案

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

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

在这个例子中没有理由使用交换。它比您建议的效率低。确实,在 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;
    }

通常, move 赋值运算符应该:
  • 销毁可见资源(尽管可能会保存实现细节资源)。
  • move 分配所有基地和成员。
  • 如果基地和成员的 move 分配没有使 rhs 资源减少,那么就这样做。

  • 像这样:
    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;
        }
    

    更新

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

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

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

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

    观察:

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

    mutex_type* __m_;
    bool __owns_;
    


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

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

    首先让我们计算默认构造函数和成员交换函数中的加载和存储:
    // 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_);
    }
    

    现在让我们通过两种方式实现 move 构造函数:
    // 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);
    }
    

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

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

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

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

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

    关于c++ - 为什么有些人使用交换进行 move 分配?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6687388/

    相关文章:

    c++ - 在 C++ Linux 中获取本地 IP 地址

    c++ - 抛出还是不抛出异常?

    c++ - 在 Visual Studio 中更改(使用旧的)c++ 版本

    c++ - 显示集合的第一个元素

    c++ - 遍历不包括最后一个元素的列表容器

    C++ 转换派生类

    c++11 - boost spirit : Sub-grammar appending to string?

    c++ - 为什么 reference 不能捕获 temporary 而 const ref 和 rval ref 可以

    c++ - 无法将右值引用函数与 GCC 匹配

    c++ - 以 std::istream&& 为参数是否合理?