目錄 Link to heading
- 目錄
什麼是 Rule of Five? Link to heading
Rule of Five(五法則) 是 C++11 引入移動語意(Move Semantics) 後,對 Rule of Three 的擴展。它指出:
如果一個類別需要自定義以下五者之一,那麼它很可能需要明確定義所有五個:
- 解構子(Destructor)
- 複製建構子(Copy Constructor)
- 複製賦值運算子(Copy Assignment Operator)
- 移動建構子(Move Constructor) ← C++11 新增
- 移動賦值運算子(Move Assignment Operator) ← C++11 新增
Rule of Five 的核心在於:當你管理資源時,除了要正確處理複製操作(Rule of Three),還應該提供高效的移動操作。
為什麼需要移動語意? Link to heading
在 C++11 之前,所有的值傳遞都涉及複製,即使是臨時物件也需要完整的深複製,這會造成不必要的效能開銷。
沒有移動語意的問題 Link to heading
#include <iostream>
#include <cstring>
class OldString {
private:
char* data;
size_t length;
public:
// 建構子
OldString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "建構: " << data << "\n";
}
// 解構子
~OldString() {
std::cout << "解構: " << data << "\n";
delete[] data;
}
// 複製建構子
OldString(const OldString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "複製建構: " << data << " (昂貴的深複製!)\n";
}
// 複製賦值運算子
OldString& operator=(const OldString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "複製賦值: " << data << " (昂貴的深複製!)\n";
}
return *this;
}
};
// 回傳臨時物件
OldString createString() {
OldString temp("Temporary String");
return temp; // 回傳時會複製!
}
int main() {
std::cout << "--- 建立臨時物件 ---\n";
OldString str = createString(); // 又一次複製!
std::cout << "\n--- 程式結束 ---\n";
return 0;
}
問題分析 Link to heading
createString() 內部:
1. 建構 temp
2. 複製 temp 到回傳值(深複製!)
3. 解構 temp
main() 內部:
4. 複製回傳值到 str(又一次深複製!)
5. 解構回傳值
結果:為了一個字串,進行了 2 次深複製!
移動語意:從複製到轉移 Link to heading
移動語意的核心概念是:不複製資源,而是轉移資源的所有權。
右值參考(Rvalue Reference) Link to heading
C++11 引入了右值參考 &&,用於識別臨時物件:
void process(const MyClass& obj); // 左值參考:接受持久物件
void process(MyClass&& obj); // 右值參考:接受臨時物件
- 左值(Lvalue):有名字的物件,存在於記憶體中,可以取地址
- 右值(Rvalue):臨時物件,即將被銷毀,無法取地址
int x = 10; // x 是左值
int y = x + 5; // x + 5 是右值(臨時結果)
MyClass obj; // obj 是左值
MyClass temp(); // temp() 的回傳值是右值
Rule of Five 的完整實作 Link to heading
#include <iostream>
#include <cstring>
#include <utility> // for std::move
class ModernString {
private:
char* data;
size_t length;
public:
// 建構子
ModernString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "建構: " << data << " (位址: " << (void*)data << ")\n";
}
// 1. 解構子
~ModernString() {
std::cout << "解構: ";
if (data) {
std::cout << data << " (位址: " << (void*)data << ")";
} else {
std::cout << "(nullptr)";
}
std::cout << "\n";
delete[] data;
}
// 2. 複製建構子(深複製)
ModernString(const ModernString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "複製建構: " << data << " (深複製)\n";
}
// 3. 複製賦值運算子
ModernString& operator=(const ModernString& other) {
std::cout << "複製賦值: " << other.data << " (深複製)\n";
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// 4. 移動建構子(轉移所有權)
ModernString(ModernString&& other) noexcept {
std::cout << "移動建構: " << other.data << " (轉移所有權)\n";
// 轉移資源
data = other.data;
length = other.length;
// 清空來源物件
other.data = nullptr;
other.length = 0;
}
// 5. 移動賦值運算子(轉移所有權)
ModernString& operator=(ModernString&& other) noexcept {
std::cout << "移動賦值: " << other.data << " (轉移所有權)\n";
// 檢查自我賦值
if (this != &other) {
// 釋放現有資源
delete[] data;
// 轉移資源
data = other.data;
length = other.length;
// 清空來源物件
other.data = nullptr;
other.length = 0;
}
return *this;
}
void print() const {
if (data) {
std::cout << "內容: " << data << "\n";
} else {
std::cout << "內容: (已移動)\n";
}
}
};
// 回傳臨時物件
ModernString createString() {
ModernString temp("Temporary String");
return temp; // 自動使用移動建構子!
}
int main() {
std::cout << "--- 1. 從函式回傳值(自動移動)---\n";
ModernString str1 = createString();
str1.print();
std::cout << "\n--- 2. 複製建構(左值)---\n";
ModernString str2 = str1; // str1 是左值,使用複製建構子
str2.print();
std::cout << "\n--- 3. 移動建構(使用 std::move)---\n";
ModernString str3 = std::move(str2); // 明確告訴編譯器:可以移動
str3.print();
str2.print(); // str2 已經被移動,內容為空
std::cout << "\n--- 4. 移動賦值 ---\n";
ModernString str4("Another String");
str4 = std::move(str3); // 移動賦值
str4.print();
str3.print();
std::cout << "\n--- 程式結束 ---\n";
return 0;
}
執行結果 Link to heading
--- 1. 從函式回傳值(自動移動)---
建構: Temporary String (位址: 0x1234)
移動建構: Temporary String (轉移所有權)
解構: (nullptr)
內容: Temporary String
--- 2. 複製建構(左值)---
複製建構: Temporary String (深複製)
內容: Temporary String
--- 3. 移動建構(使用 std::move)---
移動建構: Temporary String (轉移所有權)
內容: Temporary String
內容: (已移動)
--- 4. 移動賦值 ---
建構: Another String (位址: 0x5678)
移動賦值: Temporary String (轉移所有權)
內容: Temporary String
內容: (已移動)
--- 程式結束 ---
解構: Temporary String (位址: 0x1234)
解構: (nullptr)
解構: Another String (位址: 0x5678)
解構: (nullptr)
移動操作的核心概念 Link to heading
移動建構子 Link to heading
ModernString(ModernString&& other) noexcept {
// 1. 偷取來源物件的資源
data = other.data;
length = other.length;
// 2. 將來源物件設為有效但未定義的狀態
other.data = nullptr;
other.length = 0;
}
記憶體變化:
移動前:
source.data → [H][e][l][l][o][\0]
target.data → (未初始化)
移動後:
source.data → nullptr ← 不再擁有資源
target.data → [H][e][l][l][o][\0] ← 接管資源
為什麼要加 noexcept? Link to heading
移動操作應該標記為 noexcept,原因有:
- 效能優化:STL 容器在擴展時,如果移動建構子有
noexcept,會優先使用移動;否則為了例外安全,會使用複製 - 例外安全:移動操作通常只是指標交換,不應該拋出例外
- 語意正確:移動失敗會導致資源處於不一致狀態
std::vector<ModernString> vec;
vec.push_back(ModernString("Test"));
// 如果移動建構子有 noexcept:
// → vector 擴展時使用移動,效率高
// 如果移動建構子沒有 noexcept:
// → vector 擴展時使用複製,效率低
std::move 的作用 Link to heading
std::move 並不真的「移動」任何東西,它只是將左值轉換為右值參考,告訴編譯器「這個物件可以被移動」。
ModernString str1("Hello");
// 錯誤理解:std::move 會移動 str1
ModernString str2 = std::move(str1);
// 正確理解:
// 1. std::move(str1) 將 str1 轉換為右值參考
// 2. 編譯器看到右值參考,呼叫移動建構子
// 3. 移動建構子真正執行移動操作
使用 std::move 的時機 Link to heading
// 1. 明確表示不再使用某個物件
ModernString temp("Data");
ModernString permanent = std::move(temp);
// temp 不應該再被使用(雖然還能用,但內容已被掏空)
// 2. 從容器中移出元素
std::vector<ModernString> vec;
// ...
ModernString extracted = std::move(vec[0]);
// 3. 完美轉發(進階用法)
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
std::move 的陷阱 Link to heading
ModernString str1("Hello");
ModernString str2 = std::move(str1);
// 陷阱 1:str1 仍然是有效物件,但內容未定義
str1.print(); // 可能輸出: (已移動)
// 陷阱 2:不要在移動後繼續使用物件(除了賦值或解構)
// str1.someMethod(); // 危險!未定義行為
// 陷阱 3:const 物件無法被移動
const ModernString str3("World");
ModernString str4 = std::move(str3); // 呼叫複製建構子!不是移動!
移動建構子 vs 複製建構子的選擇 Link to heading
編譯器如何決定呼叫哪個建構子?
ModernString str1("Hello");
// 1. 複製建構(左值)
ModernString str2 = str1; // str1 是左值 → 複製建構子
// 2. 移動建構(右值)
ModernString str3 = ModernString("World"); // 臨時物件 → 移動建構子
// 3. 明確移動(左值 → 右值)
ModernString str4 = std::move(str1); // std::move → 移動建構子
// 4. 從函式回傳(自動移動)
ModernString str5 = createString(); // 回傳值 → 移動建構子
Rule of Five 的實作選項 Link to heading
選項 1:全部自定義 Link to heading
如同上面的 ModernString 範例,完整實作所有五個函式。
適用情況:
- 需要細緻控制資源管理
- 有特殊的效能需求
- 複雜的自定義資源
選項 2:使用 =default Link to heading
讓編譯器自動產生移動操作:
class MyClass {
public:
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = default;
MyClass(MyClass&&) = default; // 編譯器產生的版本通常就夠了
MyClass& operator=(MyClass&&) = default;
~MyClass() = default;
};
選項 3:只定義需要的 Link to heading
如果只需要禁止某些操作:
class NonMovable {
public:
NonMovable() = default;
~NonMovable() = default;
// 允許複製
NonMovable(const NonMovable&) = default;
NonMovable& operator=(const NonMovable&) = default;
// 禁止移動
NonMovable(NonMovable&&) = delete;
NonMovable& operator=(NonMovable&&) = delete;
};
移動語意的效能優勢 Link to heading
讓我們比較有無移動語意的效能差異:
#include <vector>
#include <chrono>
// 測量時間
template<typename Func>
void benchmark(const char* name, Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << name << ": " << duration.count() << " ms\n";
}
int main() {
const int SIZE = 1000000;
// 有移動語意
benchmark("使用移動語意", [&]() {
std::vector<ModernString> vec;
for (int i = 0; i < SIZE; ++i) {
vec.push_back(ModernString("String")); // 移動建構
}
});
// 只有複製(假設沒有移動建構子)
benchmark("只有複製", [&]() {
std::vector<OldString> vec;
for (int i = 0; i < SIZE; ++i) {
vec.push_back(OldString("String")); // 複製建構
}
});
return 0;
}
典型結果:
使用移動語意: 45 ms
只有複製: 1250 ms
效能提升: ~27 倍!
何時需要 Rule of Five? Link to heading
需要的情況(同 Rule of Three) Link to heading
- 管理動態記憶體
- 管理系統資源(檔案、網路、鎖等)
- 需要高效的移動操作
不需要的情況 Link to heading
如果你的類別:
- 只使用標準函式庫容器和智慧指標
- 不直接管理資源
則應該追求 Rule of Zero:
class Modern {
std::string name;
std::vector<int> data;
std::unique_ptr<Resource> resource;
// 什麼都不用寫!
// 編譯器自動產生的移動/複製操作已經最優化
};
實務建議 Link to heading
DO(應該做的) Link to heading
移動操作標記 noexcept
MyClass(MyClass&&) noexcept; MyClass& operator=(MyClass&&) noexcept;移動後的物件必須處於有效狀態
MyClass(MyClass&& other) noexcept { data = other.data; other.data = nullptr; // 確保來源物件可安全解構 }優先使用 std::move 轉移局部物件
std::vector<MyClass> createVector() { std::vector<MyClass> result; // ... 填充 result return result; // 編譯器自動移動,不需要 std::move }在容器操作中善用移動
std::vector<MyClass> vec; MyClass obj("Data"); vec.push_back(std::move(obj)); // 移動而非複製
DON’T(不應該做的) Link to heading
不要移動 const 物件
const MyClass obj; MyClass other = std::move(obj); // 無效!呼叫複製建構子不要在 return 語句中使用 std::move
// 錯誤 MyClass createObject() { MyClass obj; return std::move(obj); // 錯誤!阻礙 RVO(Return Value Optimization) } // 正確 MyClass createObject() { MyClass obj; return obj; // 編譯器自動優化(RVO 或移動) }不要假設移動後的物件為空
MyClass obj1("Data"); MyClass obj2 = std::move(obj1); // obj1 仍然是有效物件,但狀態未定義 // 不要假設 obj1 == nullptr 或 obj1.empty()不要遺漏 noexcept
// 不好 MyClass(MyClass&& other); // STL 容器會使用複製而非移動 // 好 MyClass(MyClass&& other) noexcept; // STL 容器會優先使用移動
Rule of Five vs Rule of Zero Link to heading
Rule of Five:手動管理 Link to heading
class ManualResource {
char* data;
public:
~ManualResource() { delete[] data; }
ManualResource(const ManualResource&); // 複製建構
ManualResource& operator=(const ManualResource&); // 複製賦值
ManualResource(ManualResource&&) noexcept; // 移動建構
ManualResource& operator=(ManualResource&&) noexcept; // 移動賦值
};
- 優點: 完全控制
- 缺點: 容易出錯,維護成本高
Rule of Zero:自動管理 Link to heading
class AutoResource {
std::unique_ptr<char[]> data; // 智慧指標自動管理一切
// 不需要寫任何特殊成員函式!
};
- 優點: 簡潔、不易出錯、自動最優化
- 缺點: 對特殊需求的控制較少
小結 Link to heading
Rule of Five 的核心概念:
Rule of Three + 移動語意 = Rule of Five
- 解構子、複製建構子、複製賦值運算子(來自 Rule of Three)
- 移動建構子、移動賦值運算子(C++11 新增)
移動語意的本質:
- 轉移資源所有權,而非複製
- 使用右值參考
&&識別臨時物件 - 透過
std::move明確表達移動意圖
實作要點:
- 移動操作必須標記
noexcept - 移動後的物件必須處於有效狀態
- 檢查自我賦值(移動賦值運算子)
- 移動操作必須標記
現代最佳實踐:
- 優先追求 Rule of Zero
- 使用智慧指標和標準函式庫容器
- 只在必要時實作 Rule of Five
效能優勢:
- 避免不必要的深複製
- 容器擴展時更高效
- 函式回傳大物件時無額外開銷
記住:在現代 C++ 中,優先使用智慧指標和 RAII,讓編譯器自動處理資源管理。只有在真正需要時,才手動實作 Rule of Five!