【问题标题】:Questions about the move assignment operator关于移动赋值运算符的问题
【发布时间】:2012-03-23 23:46:15
【问题描述】:

想象以下管理资源的类(我的问题只是关于移动赋值运算符):

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        std::swap(this->s, other.s);
        std::swap(this->p, other.p);
        return *this;
    }
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
};

问题:

上述两个移动赋值运算符#1和#2的优缺点是什么?我相信我能看到的唯一区别是std::swap 保留了 lhs 的存储,但是,我看不出这会有什么用,因为无论如何右值都会被破坏。也许唯一的一次是使用a1 = std::move(a2); 之类的东西,但即使在这种情况下,我也看不出有任何理由使用#1。

【问题讨论】:

  • 我不太明白你的问题。为什么不简单地使用std::unique_ptr<int> 成员(而不是int*)并让相关运算符自动生成或=default
  • @Walter:这个问题是一个学习实验,而不是我会在生产中使用的东西。我会选择std::vector。此外,在撰写本文时,default 并未由 MSVC 实现。
  • 很公平,但MSVC 不在标签中。

标签: c++ c++11 move move-assignment-operator


【解决方案1】:

这是一个你应该真正衡量的案例。

我正在查看 OP 的 copy 赋值运算符,发现效率低下:

A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}

如果*thisother 具有相同的s 会怎样?

在我看来,如果s == other.s,更智能的复制分配可以避免访问堆。它所要做的就是复制:

A& operator=(A const& other)
{
    if (this != &other)
    {
        if (s != other.s)
        {
            delete [] p;
            p = nullptr;
            s = 0;
            p = new int[other.s];
            s = other.s;
        }
        std::copy(other.p, other.p + s, this->p);
    }
    return *this;
}

如果您不需要强大的异常安全性,只需要复制分配的基本异常安全性(如std::stringstd::vector 等),那么使用更多。多少钱?测量。

我用三种方式编写了这个类:

设计 1:

使用上面的复制赋值运算符和 OP 的移动赋值运算符 #1。

设计 2:

使用上面的复制赋值运算符和OP的移动赋值运算符#2。

设计 3:

DeadMG 的复制赋值运算符,用于复制和移动赋值。

这是我用来测试的代码:

#include <cstddef>
#include <algorithm>
#include <chrono>
#include <iostream>

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    void swap(A& other)
    {std::swap(s, other.s); std::swap(p, other.p);}
#if DESIGN != 3
    A& operator=(A const& other)
    {
        if (this != &other)
        {
            if (s != other.s)
            {
                delete [] p;
                p = nullptr;
                s = 0;
                p = new int[other.s];
                s = other.s;
            }
            std::copy(other.p, other.p + s, this->p);
        }
        return *this;
    }
#endif
#if DESIGN == 1
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        swap(other);
        return *this;
    }
#elif DESIGN == 2
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
#elif DESIGN == 3
    A& operator=(A other)
    {
        swap(other);
        return *this;
    }
#endif
};

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<float, std::nano> NS;
    A a1(10);
    A a2(10);
    auto t0 = Clock::now();
    a2 = a1;
    auto t1 = Clock::now();
    std::cout << "copy takes " << NS(t1-t0).count() << "ns\n";
    t0 = Clock::now();
    a2 = std::move(a1);
    t1 = Clock::now();
    std::cout << "move takes " << NS(t1-t0).count() << "ns\n";
}

这是我得到的输出:

$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1  test.cpp 
$ a.out
copy takes 55ns
move takes 44ns
$ a.out
copy takes 56ns
move takes 24ns
$ a.out
copy takes 53ns
move takes 25ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2  test.cpp 
$ a.out
copy takes 74ns
move takes 538ns
$ a.out
copy takes 59ns
move takes 491ns
$ a.out
copy takes 61ns
move takes 510ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3  test.cpp 
$ a.out
copy takes 666ns
move takes 304ns
$ a.out
copy takes 603ns
move takes 446ns
$ a.out
copy takes 619ns
move takes 317ns

DESIGN 1 在我看来还不错。

警告:如果类具有需要“快速”释放的资源,例如互斥锁所有权或文件打开状态所有权,那么从正确性的角度来看,design-2 移动赋值运算符可能会更好。但是当资源只是内存时,尽可能长时间地延迟释放它通常是有利的(就像在 OP 的用例中一样)。

注意事项 2:如果您知道有其他重要的用例,请对其进行衡量。您可能会得出与我在这里不同的结论。

注意:我更看重性能而不是“干”。这里的所有代码都将封装在一个类中(struct A)。使struct A 尽可能好。而且,如果您的工作质量足够高,那么struct A 的客户(可能是您自己)就不会受到“RIA”(Reinvent It Again)的诱惑。我更喜欢在一个类中重复一些代码,而不是一遍又一遍地重复整个类的实现。

【讨论】:

  • 谢谢,这非常有用。我也对设计 #2 中的移动结果感到惊讶。
  • 看起来设计 1 相对于设计 2 的性能优势是由于测试工具没有对析构函数调用进行计时——如果你将它包含在你的计时工具中,我希望性能差异会消失.我也有点惊讶 LLVM 没有完全优化 a1 和 a2。
  • 我强烈支持 Richard Smith 的评论。这个时机是不公平,并没有真正比较相同的东西。时钟区域应该包括移动对象的破坏,因为在实践中,这几乎总是跟随移动。我很惊讶你们没有这样做。
  • @Walter:不要惊讶。考虑前面有足够容量的vector&lt;T&gt;::insert。它将对T 进行 1 次移动构造和 N-1 次移动分配,以创建插入新元素的空间。它不会破坏它移动分配的 N-1 T。如果您想了解大多数移动分配发生在哪里(以及源是否很快被破坏),请查看您的 std-lib 的底层,或与有的人交谈。
【解决方案2】:

使用 #1 比使用 #2 更有效,因为如果使用 #2,您将违反 DRY 并复制您的析构函数逻辑。其次,考虑以下赋值运算符:

A& operator=(A other) {
    swap(*this, other);
    return *this;
}

这既是复制又是移动赋值运算符,不会出现重复代码 - 一种极好的形式。

【讨论】:

  • 为了提问者,什么是 DRY?
  • 谢谢,我没有考虑这个角度。
  • DRY = "不要重复自己"
  • @DeadMG - 为什么要复制和移动呢? move() 不需要其他通过右值引用传递的吗?如果不是,为什么?
  • @lori:因为other参数可以由任何隐式构造函数构造——移动和复制都是。
【解决方案3】:

如果swap()ing 涉及的对象不能抛出,DeadMG 发布的赋值运算符是正确的。不幸的是,这不能总是得到保证!特别是,如果您有有状态的分配器,这将不起作用。如果分配器可以不同,那么您似乎需要单独的复制和移动分配:复制构造函数将无条件地创建一个在分配器中传递的副本:

T& T::operator=(T const& other) {
    T(other, this->get_allocator()).swap(*this);
    return * this;
}

移动分配将测试分配器是否相同,如果是,则只需swap() 两个对象,否则仅调用复制分配:

T& operator= (T&& other) {
    if (this->get_allocator() == other.get_allocator()) {
        this->swap(other);
    }
    else {
        *this = other;
    }
    return *this;
}

取值的版本是一个更简单的替代方案,如果noexcept(v.swap(*this))true,则应该首选它。

这也隐含地回答了原始问题:在抛出swap() 和移动赋值的情况下,这两个实现都是错误的,因为它们不是基本的异常安全的。假设swap() 中的唯一异常来源是不匹配的分配器,那么上面的实现是强异常安全的。

【讨论】:

    猜你喜欢
    • 2011-08-12
    • 1970-01-01
    • 2021-08-18
    • 2014-05-24
    • 2017-11-21
    • 1970-01-01
    • 1970-01-01
    • 2011-03-06
    • 2015-10-14
    相关资源
    最近更新 更多