更多 C++ 慣用法/移動建構函式
在 C++03 中將一個物件持有的資源的所有權轉移到另一個物件。注意:在 C++11 中,移動建構函式是使用內建的右值引用功能實現的。
- Colvin-Gibbons 技巧
- 源和匯技巧
C++ 中的一些物件表現出所謂的移動語義。例如,std::auto_ptr。在下面的程式碼中,auto_ptr b 在建立物件 a 後不再有用。
std::auto_ptr <int> b (new int (10));
std::auto_ptr <int> a (b);
auto_ptr 的複製建構函式修改了它的引數,因此它不接受 const 引用作為引數。它在上面的程式碼中沒有問題,因為物件 b 不是 const 物件。但它在涉及臨時物件時會產生問題。
當一個函式按值返回一個物件,並且該返回的物件用作函式的引數(例如,構造另一個相同類的物件)時,編譯器會建立一個返回物件的臨時物件。這些臨時物件的生命週期很短,一旦語句執行完畢,臨時物件的解構函式就會被呼叫。因此,臨時物件在其非常短的生命週期內擁有其資源。問題是,臨時物件非常類似於 const 物件(修改臨時物件幾乎沒有意義)。因此,它們不能繫結到非 const 引用,因此不能用於呼叫接受非 const 引用的建構函式。移動建構函式可用於此類情況。
namespace detail {
template <class T>
struct proxy
{
T *resource_;
};
} // detail
template <class T>
class MovableResource
{
private:
T * resource_;
public:
explicit MovableResource (T * r = 0) : resource_(r) { }
~MovableResource() throw() { delete resource_; } // Assuming std::auto_ptr like behavior.
MovableResource (MovableResource &m) throw () // The "Move constructor" (note non-const parameter)
: resource_ (m.resource_)
{
m.resource_ = 0; // Note that resource in the parameter is moved into *this.
}
MovableResource (detail::proxy<T> p) throw () // The proxy move constructor
: resource_(p.resource_)
{
// Just copying resource pointer is sufficient. No need to NULL it like in the move constructor.
}
MovableResource & operator = (MovableResource &m) throw () // Move-assignment operator (note non-const parameter)
{
// copy and swap idiom. Must release the original resource in the destructor.
MovableResource temp (m); // Resources will be moved here.
temp.swap (*this);
return *this;
}
MovableResource & operator = (detail::proxy<T> p) throw ()
{
// copy and swap idiom. Must release the original resource in the destructor.
MovableResource temp (p);
temp.swap(*this);
return *this;
}
void swap (MovableResource &m) throw ()
{
std::swap (this->resource_, m.resource_);
}
operator detail::proxy<T> () throw () // A helper conversion function. Note that it is non-const
{
detail::proxy<T> p;
p.resource_ = this->resource_;
this->resource_ = 0; // Resource moved to the temporary proxy object.
return p;
}
};
移動建構函式/賦值慣用法在接下來的程式碼片段中扮演著重要角色。函式 func 按值返回物件,即返回一個臨時物件。雖然 MovableResource 沒有任何複製建構函式,但 main 中的區域性變數 a 的構造成功,同時將所有權從臨時物件移走。
MovableResource<int> func()
{
MovableResource<int> m(new int());
return m;
}
int main()
{
MovableResource<int> a(func()); // Assuming this call is not return value optimized (RVO'ed).
}
該慣用法結合了 C++ 的三個有趣且標準的特性。
- 複製建構函式不必按
const引用接受其引數。C++ 標準在 12.8.2 節中提供了複製建構函式的定義。引用--類 X 的非模板建構函式是複製建構函式,如果它的第一個引數是X&、const X&、volatile X&或const volatile X&型別,並且要麼沒有其他引數,要麼所有其他引數都有預設引數。--結束引用。 - 編譯器會自動識別透過
detail::proxy<T>物件進行的一系列轉換。 - 非
const函式可以在臨時物件上呼叫。例如,轉換函式operator detail::proxy<T> ()不是const。此成員轉換運算子用於修改臨時物件。
編譯器會尋找一個複製建構函式來初始化物件 a。有一個複製建構函式已定義,但它按非 const 引用接受其引數。非 const 引用不會繫結到函式 func 返回的臨時物件。因此,編譯器會尋找其他選項。它發現提供了一個接受 detail::proxy<T> 物件的建構函式。因此它嘗試識別一個將 MovableResource 物件轉換為 detail::proxy<T> 的轉換運算子。事實上,也提供了這樣的轉換運算子(operator detail::proxy<T>())。還要注意,轉換運算子不是 const。這不是問題,因為 C++ 允許在臨時物件上呼叫非 const 函式。呼叫此轉換函式後,區域性 MovableResource 物件 (m) 會失去其資源所有權。只有 proxy<T> 物件在很短的時間內知道指向 T 的指標。隨後,轉換建構函式(接受 detail::proxy<T> 作為引數的建構函式)成功地獲得了所有權(main 中的 a 物件)。
讓我們深入瞭解一下臨時物件是如何建立和使用的。事實上,上面的步驟不是執行一次,而是以完全相同的方式執行兩次。首先是建立一個臨時 MovableResource 物件,然後是建立 main 中的最終物件 a。當從臨時 MovableResource 物件建立 a 時,C++ 的第二個例外規則開始發揮作用。轉換函式 (operator detail::proxy<T>()) 會在臨時 MovableResource 物件上呼叫。注意,它不是 const。與真正的 const 物件不同,非 const 成員函式可以在臨時物件上呼叫。C++ ISO/IEC 14882:1998 標準的 3.10.10 節明確提到了這個例外。有關此例外的更多資訊,請參見這裡。轉換運算子恰好是一個非 const 成員函式。因此,臨時 MovableResource 物件也會像上面描述的那樣失去所有權。當編譯器找出正確的轉換函式序列時,也會建立和銷燬幾個臨時 proxy<T> 物件。編譯器可能會使用返回值最佳化 (RVO) 來消除某些臨時物件。要觀察所有臨時物件,請對 gcc 編譯器使用 --no-elide-constructors 選項。
這種移動建構函式慣用法的實現對於在函式內外轉移資源(例如,堆分配的記憶體)非常有用。接受 MovableResource 按值作為引數的函式充當匯函式,因為所有權被轉移到呼叫堆疊中更深層的函式,而按值返回 MovableResource 的函式充當源,因為所有權被移動到外部範圍。因此,名稱為源和匯慣用法。
安全移動建構函式
雖然源和匯慣用法在函式呼叫的邊界處非常有用,但上述實現有一些不希望有的副作用。特別是,看起來像複製賦值和複製初始化的簡單表示式根本不會進行復制。例如,考慮以下程式碼
MovableResource mr1(new int(100));
MovableResource mr2;
mr2 = mr1;
MovableResource 物件 mr2 看起來是從 mr1 複製賦值的。但是,它會悄無聲息地從 mr1 竊取底層資源,使其像預設初始化一樣被遺留下來。mr1 物件不再擁有堆分配的整數。這種行為讓大多數程式設計師感到驚訝。此外,假設常規復制語義編寫的通用程式碼很可能會產生非常不希望有的後果。
上面提到的語義通常稱為移動語義。它是一個強大的最佳化。但是,這種最佳化以上述形式實現的部分問題在於,對於看起來像複製但實際上執行了移動的賦值和複製初始化表示式,缺乏編譯期間的錯誤資訊。具體來說,隱式地從另一個命名 MovableResource(例如,mr1)複製一個 MovableResource 是不安全的,因此應該以錯誤的形式表示。
請注意,從臨時物件中竊取資源是完全可以接受的,因為它們的持續時間不長。這種最佳化是為了在右值(臨時物件)上使用。安全移動建構函式慣用法是在沒有語言級支援(C++11 中的右值引用)的情況下實現這種區別的一種方法。
安全移動建構函式慣用法透過宣告私有建構函式和賦值運算子函式來防止從命名物件中隱式移動語義。
private:
MovableResource (MovableResource &m) throw ();
MovableResource & operator = (MovableResource &m) throw ();
這裡需要注意的關鍵是引數型別是非 const 的。它們只繫結到命名物件,而不是臨時物件。僅此一項小小的改變就足以停用從命名物件的隱式移動,但允許從臨時物件的移動。該慣用法還提供了一種方法,如果需要,可以從命名物件進行顯式移動。
template <class T>
MovableResource<T> move(MovableResource<T> & mr) throw() // Convert explicitly to a non-const reference to rvalue
{
return MovableResource<T>(detail::proxy<T>(mr));
}
MovableResource<int> source()
{
MovableResource<int> local(new int(999));
return move(local);
}
void sink(MovableResource<int> mr)
{
// Do something with mr. mr is deleted automatically at the end.
}
int main(void)
{
MovableResource<int> mr(source()); // OK
MovableResource<int> mr2;
mr2 = mr; // Compiler error
mr2 = move(mr); // OK
sink(mr2); // Compiler error
sink(move(mr2)); // OK
}
上面的 move 函式也是非常慣用的。它必須接受引數作為物件的非 const 引用,以便它僅繫結到左值。它透過轉換為 detail::proxy 將非 const MovableResource 左值(命名物件)轉換為右值(臨時物件)。在 move 函式內部需要進行兩次顯式轉換以避免 某些語言怪癖。理想情況下,move 函式應該與 MovableResource 類定義在相同的名稱空間中,這樣就可以使用引數依賴查詢 (ADL) 查詢該函式。
函式 source 返回一個區域性 MovableResource 物件。因為它是一個命名物件,所以在返回之前必須使用 move 將其轉換為右值。在 main 中,首先 MovableResource 直接從 func 返回的臨時物件初始化。簡單地賦值給另一個 MovableResource 會導致編譯器錯誤,但顯式移動可以。類似地,隱式複製到 sink 函式不起作用。程式設計師必須明確表達將物件移動到 sink 函式的意願。
最後,重要的是關鍵函式(從 detail::proxy 構建以及到 detail::proxy 構建)必須是非丟擲函式,以保證至少基本的異常保證。在此期間不應該丟擲任何異常,否則會導致資源洩漏。
C++11 提供了 右值引用,它支援語言級的移動語義,消除了對移動建構函式模式的需要。
歷史上的替代方案
一個老舊的(但較差的)替代方案是使用一個 mutable 成員來跟蹤所有權,從而繞過編譯器的 const 正確性檢查。以下程式碼展示瞭如何以另一種方式實現 MovableResource。
template<class T>
class MovableResource
{
mutable bool owner;
T* px;
public:
explicit MovableResource(T* p=0)
: owner(p), px(p) {}
MovableResource(const MovableResource& r)
: owner(r.owner), px(r.release()) {}
MovableResource & operator = (const MovableResource &r)
{
if ((void*)&r != (void*)this)
{
if (owner)
delete px;
owner = r.owner;
px = r.release();
}
return *this;
}
~MovableResource() { if (owner) delete px; }
T& operator*() const { return *px; }
T* operator->() const { return px; }
T* get() const { return px; }
T* release() const { owner = false; return px; } // mutable 'ownership' changed here.
};
然而,這種技術有幾個缺點。
- 複製建構函式和複製賦值運算子不會進行邏輯上的 **複製**。相反,它們將所有權從右側物件轉移到
*this。MovableResource的第二個實現的介面中沒有反映這一點。第一個MovableResource類透過非 const 引用接受MovableResource物件,這會阻止在預期複製的上下文中使用該物件。 - Const auto_ptr 模式是 C++03 中防止所有權轉移的方式,在
MovableResource類的第二個實現中根本不可能使用。這種模式依賴於constauto_ptr 物件不能繫結到移動建構函式和移動賦值運算子的非 const 引用引數這一事實。將引數設為const引用會違背目的。 boolean標誌 'owner' 會增加結構的大小。對於像std::auto_ptr這樣的類來說,大小的增加是相當大的(實際上是兩倍),而這些類本身只包含一個指標。大小翻倍是由於編譯器強制執行的資料對齊。
已知用途
[edit | edit source]- Boost.Move 庫
- std::auto_ptr 使用移動建構函式的“不安全”版本。
- Howard Hinnant 的 unique_ptr C++03 模擬 使用安全的移動建構函式模式。