2012-03-23 97 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()中唯一的異常來源是不匹配的分配器,上面的實現是強大的異常安全。