有关移动赋值运算符的问题

Jes*_*ood 3 c++ move c++11 move-assignment-operator

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

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;
     } 
};
Run Code Online (Sandbox Code Playgroud)

题:

上面两个移动分配操作符#1和#2的优点和缺点是什么?我相信我能看到的唯一区别就是std::swap保留了lhs的存储空间,然而,我不知道这有什么用,因为rvalues无论如何都会被破坏.也许唯一的时间就是这样a1 = std::move(a2);,但即使在这种情况下,我也没有看到任何理由使用#1.

How*_*ant 8

这是你应该真正衡量的情况.

而且我正在查看OP的复制赋值运算符并发现效率低下:

A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}
Run Code Online (Sandbox Code Playgroud)

如果*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;
}
Run Code Online (Sandbox Code Playgroud)

如果没有需要强异常安全,在拷贝赋值只有基本异常安全(如std::string,std::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";
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的输出:

$ 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
Run Code Online (Sandbox Code Playgroud)

DESIGN 1 看起来对我很好.

警告:如果类具有需要"快速"解除分配的资源,例如互斥锁所有权或文件开放状态所有权,则从正确的角度来看,设计2移动赋值运算符可能更好.但是当资源只是内存时,通常有利的是尽可能延迟解除分配(如OP的用例).

警告2:如果您知道其他用例很重要,请测量它们.你可能会得出与我在这里不同的结论.

注意:我重视"干"的表现.这里的所有代码都将封装在一个类(struct A)中.尽struct A你所能.如果你做了一份足够高质量的工作,那么你struct A(或许你自己)的客户就不会想要"RIA"(再次重塑它).我更喜欢在一个类中重复一些代码,而不是一遍又一遍地重复整个类的实现.

  • 看起来设计1优于设计2的性能优势是由于测试工具未对析构函数调用进行计时 - 如果将其包含在时序线束中,我预计性能差异将消失.我也有点惊讶LLVM没有完全优化a1和a2. (2认同)

Pup*_*ppy 7

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

A& operator=(A other) {
    swap(*this, other);
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

这是复制和移动赋值操作符,没有重复的代码 - 一种优秀的形式.

  • @lori:因为`other` 参数可以由任何隐式构造函数构造——移动和复制都是。 (2认同)