5

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

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。

4

3 回答 3

9

这是您应该真正衡量的情况。

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

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等),那么上述内容可能会提高性能。多少?措施。

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

设计一:

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

设计二:

使用上面的复制赋值运算符和 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)的诱惑。我更喜欢在一个类中重复一些代码,而不是一遍又一遍地重复整个类的实现。

于 2012-03-24T02:48:01.577 回答
8

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

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

这既是复制又是移动赋值运算符,没有重复的代码——一种很好的形式。

于 2012-03-23T23:49:26.330 回答
3

swap()如果涉及的对象不能抛出,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()是不匹配的分配器,上面的实现是强异常安全的。

于 2012-03-24T00:15:02.587 回答