2012-03-23 99 views
3

试想一下,管理一个资源的以下类问题(我的问题是只有移动赋值运算符):的举动赋值运算符

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的存储空间,但是,我不明白这是如何有用的,因为rvalues会被破坏。也许唯一的时间会是像a1 = std::move(a2);,但即使在这种情况下,我没有看到任何理由使用#1。

+0

我不完全明白你的问题。你为什么不简单地使用'std :: unique_ptr'成员(而不是'int *'),并让所讨论的操作符是自动生成的还是'= default'? – Walter 2015-11-19 09:12:30

+0

@Walter:问题是一个学习实验,而不是我在生产中使用的东西。我会选择'std :: vector'来代替。另外,在撰写本文时,默认值并未由MSVC实现。 – 2015-11-19 10:22:14

+0

不够公平,但'MSVC'不在标签中。 – Walter 2015-11-19 13:40:07

回答

7

这是一个你应该真正测量的情况。

而且我看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等),然后有一个上述潜在的性能改进。多少?测量。

我编写这个类三种方式:

设计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看起来相当不错。警告:如果类需要“快速”释放资源,例如互斥锁的所有权或文件的开放状态所有权,则从正确性的角度来看,设计2移动赋值运算符可能会更好。但是,当资源只是内存时,通常有利的是尽可能延迟释放资源(如OP的用例)。

注意事项2:如果您还有其他用例,您知道它们很重要,请测量它们。你可能得出不同的结论,比我在这里。

注:我比“DRY”更看重表现。这里的所有代码将被封装在一个类中(struct A)。使struct A尽可能好。如果你做的工作质量足够高,那么你的客户struct A(可能是你自己)不会被诱惑到“RIA”(再次重塑它)。我更喜欢在一个类中重复一个小代码,而不是一遍又一遍地重复执行整个类。

+0

谢谢,这是非常丰富的。我对设计#2的成果感到惊讶。 – 2012-03-24 11:19:39

+2

看起来设计1的性能优势超过设计2是由于测试工具没有对析构函数调用进行计时造成的 - 如果将它包含在时序工具中,我会期望性能差异消失。我也有点惊讶LLVM没有完全优化a1和a2。 – 2012-05-28 23:42:26

+0

我强烈支持理查德史密斯的评论。这个时间是**不公平**,并没有真正比较相同的事情。时钟区域应该包括从被移动物体的破坏,因为在实践中,这将几乎总是跟着移动。我很惊讶你们都没有这样做。 – Walter 2015-11-19 09:18:37

7

使用#1而不是#2更有效,因为如果您使用#2,那么您违反DRY并复制了析构函数逻辑。其次,考虑下面的赋值操作符:

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

这既是复制和移动赋值运算符的没有重复代码 - 一个很好的形式。

+0

为了提问者,干什么? – 2012-03-23 23:55:18

+0

谢谢,这是一个我没有考虑的角度。 – 2012-03-24 00:07:48

+1

DRY =“不要重复自己” – 2012-03-24 00:13:11

3

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()中唯一的异常来源是不匹配的分配器,上面的实现是强大的异常安全。