Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++ 左右值參考與 Rust 所有權系統比較

C++ 基本概念

左值與右值

左值(lvalue):可以取地址的表達式,通常代表一個持久的物件

  • 例如:變數名稱、陣列元素、解參考的指標等

右值(rvalue):暫時的、即將消失的值

  • 例如:字面值、臨時物件、函數返回的臨時值等

參考類型

左值參考(Lvalue Reference)- 使用 &

int x = 10;
int& ref = x;  // 左值參考,綁定到 x
ref = 20;      // 修改 x 的值

記憶體示意圖:

┌─────────────────────────────────────────┐
│  記憶體位址: 0x1000                      │
│  ┌───────────┐                           │
│  │   x: 10   │ ← 原始變數                │
│  └───────────┘                           │
│       ↑                                  │
│       │                                  │
│       │ (ref 是 x 的別名,指向同一位址)    │
│       │                                  │
│  ┌───────────┐                           │
│  │  ref: &x  │ ← 左值參考(綁定到 x)       │
│  └───────────┘                           │
└─────────────────────────────────────────┘

操作流程:
  ref = 20  →  直接修改 x 的值

  結果:
  ┌───────────┐
  │   x: 20   │
  └───────────┘
       ↑
       │
  ┌───────────┐
  │  ref: &x  │
  └───────────┘

右值參考(Rvalue Reference)- 使用 &&(C++11 引入)

int&& rref = 10;           // 右值參考,綁定到臨時值
int&& rref2 = getValue();  // 綁定到函數返回的臨時物件

記憶體示意圖:

場景 1: 綁定到字面值
═══════════════════════════════════════════
  int&& rref = 10;

  ┌─────────────────────────────────────┐
  │  編譯器創建臨時物件                  │
  │  ┌───────────┐                       │
  │  │ 臨時值: 10 │ ← 右值(即將消失)      │
  │  └───────────┘                       │
  │       ↑                              │
  │       │ (延長生命週期)                │
  │       │                              │
  │  ┌────────────┐                      │
  │  │ rref: &&   │ ← 右值參考            │
  │  └────────────┘                      │
  └─────────────────────────────────────┘


場景 2: 移動語意
═══════════════════════════════════════════
  std::string s1 = "Hello";
  std::string s2 = std::move(s1);

  移動前:
  ┌──────────────────────────────────────┐
  │  s1:                                 │
  │  ┌────────┐        ┌──────────────┐ │
  │  │ ptr    │───────→│ "Hello"      │ │
  │  │ size:5 │        │ (heap memory)│ │
  │  └────────┘        └──────────────┘ │
  └──────────────────────────────────────┘

  std::move(s1) 將 s1 轉型為右值參考:
  ┌──────────────────────────────────────┐
  │  std::string&& temp = std::move(s1)  │
  │                                      │
  │  允許 s2 的移動建構函數「竊取」資源   │
  └──────────────────────────────────────┘

  移動後:
  ┌──────────────────────────────────────┐
  │  s1: (已被移動)                      │
  │  ┌────────┐                          │
  │  │nullptr │ ← 資源已被轉移            │
  │  │ size:0 │                          │
  │  └────────┘                          │
  │                                      │
  │  s2: (接管資源)                      │
  │  ┌────────┐        ┌──────────────┐ │
  │  │ ptr    │───────→│ "Hello"      │ │
  │  │ size:5 │        │ (同一塊記憶體)│ │
  │  └────────┘        └──────────────┘ │
  └──────────────────────────────────────┘

左值參考 vs 右值參考 比較圖:

┌────────────────────────────────────────────────────────────────┐
│                    左值參考 (&)                                 │
├────────────────────────────────────────────────────────────────┤
│  int x = 10;                                                   │
│  int& lref = x;     ✓ 綁定到左值                               │
│  int& lref = 10;    ✗ 錯誤! 不能綁定到右值                      │
│                                                                │
│  用途: 別名、避免複製、修改原始物件                              │
│                                                                │
│  生命週期: 必須綁定到已存在的物件                               │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│                    右值參考 (&&)                                │
├────────────────────────────────────────────────────────────────┤
│  int x = 10;                                                   │
│  int&& rref = x;         ✗ 錯誤! 不能綁定到左值                 │
│  int&& rref = 10;        ✓ 綁定到右值                          │
│  int&& rref = x + 5;     ✓ 綁定到臨時值                        │
│  int&& rref = std::move(x);  ✓ 強制轉型為右值                  │
│                                                                │
│  用途: 移動語意、完美轉發、資源轉移                              │
│                                                                │
│  生命週期: 延長臨時物件的生命、允許"竊取"資源                    │
└────────────────────────────────────────────────────────────────┘

函數重載範例:
═══════════════════════════════════════════════════════════════
  void process(int& x)   { cout << "左值版本\n"; }
  void process(int&& x)  { cout << "右值版本\n"; }

  int a = 10;
  process(a);           →  呼叫 process(int&)
  process(20);          →  呼叫 process(int&&)
  process(a + 5);       →  呼叫 process(int&&)
  process(std::move(a)); →  呼叫 process(int&&)

為什麼需要右值參考?

1. 移動語意(Move Semantics)

class MyString {
    char* data;
public:
    // 移動建構函數
    MyString(MyString&& other) noexcept {
        data = other.data;      // 直接「偷」資源
        other.data = nullptr;   // 清空來源
    }
    
    // 移動賦值運算子
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

2. 完美轉發(Perfect Forwarding)

template<typename T>
void wrapper(T&& arg) {
    // 保持參數的左值/右值屬性
    actualFunction(std::forward<T>(arg));
}

實際例子

#include <iostream>
#include <vector>

void process(int& x) {
    std::cout << "左值參考版本\n";
}

void process(int&& x) {
    std::cout << "右值參考版本\n";
}

int main() {
    int a = 10;
    process(a);        // 呼叫左值參考版本
    process(20);       // 呼叫右值參考版本
    process(a + 5);    // 呼叫右值參考版本(臨時值)
    
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = std::move(v1);  // 使用移動語意,高效率
}

關鍵優勢

使用右值參考和移動語意可以避免不必要的複製,大幅提升效能,特別是處理大型物件(如容器、字串)時。這是現代 C++ 效能優化的核心技術之一。


為什麼右值參考是效能優化工具?

核心問題:昂貴的複製操作

在沒有右值參考之前,C++ 會進行深層複製(Deep Copy)

class BigData {
    int* data;
    size_t size;
public:
    BigData(size_t s) : size(s) {
        data = new int[size];
        // 假設這裡有大量資料
    }
    
    // 傳統複製建構函數
    BigData(const BigData& other) : size(other.size) {
        data = new int[size];
        // 複製所有資料!這很慢!
        for (size_t i = 0; i < size; ++i) {
            data[i] = other.data[i];
        }
    }
    
    ~BigData() {
        delete[] data;
    }
};

BigData createBigData() {
    BigData temp(1000000);  // 100萬個整數
    return temp;  // 返回時會觸發複製!
}

int main() {
    BigData obj = createBigData();  // 又一次複製!
    // 總共可能發生多次昂貴的複製操作
}

沒有移動語意時的流程:

1. 在函數內創建 temp          → 分配記憶體 + 初始化
2. 返回時複製到臨時物件        → 再次分配記憶體 + 複製所有資料
3. 賦值給 obj                 → 第三次分配記憶體 + 複製所有資料
4. 銷毀臨時物件               → 釋放記憶體

時間複雜度:O(n) × 複製次數,其中 n 是資料大小

右值參考的優化方案

使用移動語意,我們可以直接轉移資源而不是複製:

class BigData {
    int* data;
    size_t size;
public:
    // ... 其他程式碼 ...
    
    // 移動建構函數 - 關鍵優化!
    BigData(BigData&& other) noexcept : data(other.data), size(other.size) {
        // 只是「偷走」指標,不複製資料!
        other.data = nullptr;
        other.size = 0;
    }
    
    // 移動賦值運算子
    BigData& operator=(BigData&& other) noexcept {
        if (this != &other) {
            delete[] data;  // 清理舊資料
            
            // 直接接管資源
            data = other.data;
            size = other.size;
            
            // 清空來源
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

int main() {
    BigData obj = createBigData();  // 現在使用移動,超快!
}

使用移動語意後的流程:

1. 在函數內創建 temp          → 分配記憶體 + 初始化
2. 返回時移動到臨時物件        → 只轉移指標(幾個位元組)
3. 移動到 obj                 → 只轉移指標(幾個位元組)
4. 銷毀臨時物件               → 什麼都不用做(已經是 nullptr)

時間複雜度:O(1)

實際效能對比

#include <iostream>
#include <chrono>
#include <vector>

class Timer {
    std::chrono::time_point<std::chrono::high_resolution_clock> start;
public:
    Timer() : start(std::chrono::high_resolution_clock::now()) {}
    
    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
        std::cout << "耗時: " << duration.count() << " ms\n";
    }
};

int main() {
    const size_t SIZE = 10000000;  // 1000萬個整數
    
    std::cout << "測試複製操作:\n";
    {
        Timer t;
        std::vector<int> v1(SIZE, 42);
        std::vector<int> v2 = v1;  // 複製!需要分配新記憶體並複製所有元素
    }
    
    std::cout << "\n測試移動操作:\n";
    {
        Timer t;
        std::vector<int> v1(SIZE, 42);
        std::vector<int> v2 = std::move(v1);  // 移動!只轉移指標
    }
}

典型輸出

測試複製操作:
耗時: 45 ms

測試移動操作:
耗時: 0 ms

為什麼能優化?

1. 避免記憶體分配

// 複製:需要 new
BigData copy = original;  // new int[1000000]

// 移動:不需要 new
BigData moved = std::move(original);  // 只是 pointer = other.pointer

2. 避免資料複製

// 複製:O(n) 時間
for (int i = 0; i < 1000000; ++i) {
    data[i] = other.data[i];
}

// 移動:O(1) 時間
data = other.data;  // 一個指標賦值

3. 減少解構函數呼叫的工作

// 複製後的臨時物件:需要 delete[] 大量記憶體
~BigData() { delete[] data; }

// 移動後的臨時物件:data 已經是 nullptr,什麼都不用做
~BigData() { delete[] data; }  // delete nullptr 是安全且快速的

實際應用場景

1. 容器操作

std::vector<std::string> v;
std::string s = "very long string...";

v.push_back(s);              // 複製,s 仍然有效
v.push_back(std::move(s));   // 移動,快很多!s 變成空字串

2. 工廠函數

std::unique_ptr<BigObject> createObject() {
    auto obj = std::make_unique<BigObject>();
    // ... 初始化 ...
    return obj;  // 自動移動,不複製
}

3. swap 操作

// 傳統 swap:需要 3 次複製
void swap(T& a, T& b) {
    T temp = a;    // 複製
    a = b;         // 複製
    b = temp;      // 複製
}

// 現代 swap:只需要 3 次移動
void swap(T& a, T& b) {
    T temp = std::move(a);    // 移動
    a = std::move(b);         // 移動
    b = std::move(temp);      // 移動
}

效能優化總結

右值參考是效能優化工具,因為它:

  1. 將 O(n) 的複製操作變成 O(1) 的指標轉移
  2. 避免不必要的記憶體分配和釋放
  3. 減少 CPU 快取未命中(cache miss)
  4. 特別適合處理大型物件:字串、容器、資源包裝器等

這就是為什麼現代 C++ 程式碼中到處都能看到 std::move 的原因!


Rust 的所有權系統

Rust 有更嚴格但更安全的類似概念,稱為所有權(Ownership)移動語意(Move Semantics)

基本所有權

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的所有權「移動」到 s2
    
    // println!("{}", s1);  // 編譯錯誤!s1 已經無效
    println!("{}", s2);     // 正常運作
}

Rust 的特點

  • 預設就是移動:賦值或傳參數時,預設轉移所有權
  • 借用(Borrowing):類似 C++ 的參考,但有編譯期檢查
#![allow(unused)]
fn main() {
fn process(s: &String) {  // 不可變借用
    println!("{}", s);
}

fn modify(s: &mut String) {  // 可變借用
    s.push_str(" world");
}
}
  • 沒有運行時開銷:所有檢查都在編譯期完成

其他語言的對比

Java / C# / Python

  • 物件都是參考(reference),不需要區分左右值
  • 依賴垃圾回收(GC),沒有手動的移動語意
  • 方便但可能有效能開銷
# Python - 一切都是參考
a = [1, 2, 3]
b = a  # b 和 a 指向同一個物件,沒有複製

Go

  • 有指標和值的概念
  • 但沒有明確的左右值參考語法
  • 編譯器會自動優化

Swift

  • inout 參數(類似參考)
  • 值類型(struct)和參考類型(class)的區別
  • Copy-on-Write 優化
func modify(_ value: inout Int) {
    value += 10
}

Rust vs C++ 核心差異

特性C++Rust
安全性程式設計師負責編譯器強制保證
移動語意需明確使用 std::move預設行為
參考檢查運行時可能出錯編譯期保證正確
靈活性更高(也更危險)受限但更安全

C++ - 可能的危險程式碼

int* ptr = new int(10);
delete ptr;
*ptr = 20;  // 運行時錯誤!dangling pointer

Rust - 編譯器阻止錯誤

#![allow(unused)]
fn main() {
let v = vec![1, 2, 3];
let ptr = &v[0];
drop(v);           // 編譯錯誤!
// println!("{}", ptr);  // ptr 會變成 dangling reference
}

總結

  • C++:靈活但需要小心,右值參考是效能優化工具
  • Rust:安全第一,所有權系統從根本上防止記憶體錯誤
  • 其他高階語言:用 GC 換取便利性,犧牲一些效能

Rust 可以說是把 C++ 的移動語意概念「做到極致」,並在編譯期就保證記憶體安全。


延伸閱讀