目錄 Link to heading

什麼是物件切割? Link to heading

物件切割(Object Slicing) 是 C++ 中一個常見且危險的問題,發生在當派生類(derived class)物件被賦值給基礎類(base class)物件時,派生類特有的部分會被「切掉」

簡單來說,當你用 pass-by-value 的方式將子類物件傳給父類物件時,子類獨有的資料和行為都會消失。

問題範例 Link to heading

讓我們看一個具體的例子:

#include <iostream>
#include <string>

class Animal {
public:
    std::string name;

    virtual void makeSound() const {
        std::cout << "Some generic animal sound\n";
    }

    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    std::string breed;  // Dog 特有的成員變數

    virtual void makeSound() const override {
        std::cout << "Woof! Woof!\n";
    }
};

int main() {
    Dog myDog;
    myDog.name = "Buddy";
    myDog.breed = "Golden Retriever";

    std::cout << "原始 Dog 物件:\n";
    std::cout << "Name: " << myDog.name << "\n";
    std::cout << "Breed: " << myDog.breed << "\n";
    myDog.makeSound();  // 輸出: Woof! Woof!

    std::cout << "\n--- 物件切割發生 ---\n\n";

    // 物件切割發生了!
    Animal animal = myDog;  // pass-by-value,呼叫 Animal 的複製建構子

    std::cout << "切割後的 Animal 物件:\n";
    std::cout << "Name: " << animal.name << "\n";
    // std::cout << "Breed: " << animal.breed << "\n";  // 編譯錯誤!breed 不存在
    animal.makeSound();  // 輸出: Some generic animal sound
                        // 不是 "Woof!"!多型失效了!

    return 0;
}

輸出結果 Link to heading

原始 Dog 物件:
Name: Buddy
Breed: Golden Retriever
Woof! Woof!

--- 物件切割發生 ---

切割後的 Animal 物件:
Name: Buddy
Some generic animal sound

發生了什麼事? Link to heading

當執行 Animal animal = myDog; 時,發生了以下情況:

1. 資料遺失 Link to heading

  • Dogbreed 成員變數完全消失
  • animal 物件只包含 Animal 部分的資料
  • 這是因為複製建構時,只複製了基礎類的大小

2. 多型失效 Link to heading

  • 雖然 makeSound() 是虛擬函式
  • 但因為 animalAnimal 型別(不是指標或參考)
  • 所以呼叫的是 Animal::makeSound(),而非 Dog::makeSound()
  • 虛擬函式的多型機制失效

3. 記憶體佈局 Link to heading

myDog 的記憶體佈局:
┌─────────────────┐
│ vptr (virtual)  │  ← 指向 Dog 的 vtable
├─────────────────┤
│ name (Animal)   │
├─────────────────┤
│ breed (Dog)     │  ← Dog 特有的資料
└─────────────────┘

animal 的記憶體佈局(切割後):
┌─────────────────┐
│ vptr (virtual)  │  ← 指向 Animal 的 vtable(不是 Dog 的!)
├─────────────────┤
│ name (Animal)   │
└─────────────────┘
                     ← breed 被切掉了!

容易發生切割的場景 Link to heading

場景 1:函式參數傳遞(最常見) Link to heading

// 危險:會發生切割
void processAnimal(Animal animal) {  // pass-by-value
    animal.makeSound();  // 總是呼叫 Animal::makeSound()
}

int main() {
    Dog myDog;
    processAnimal(myDog);  // 切割發生!傳入時就切掉了
}

場景 2:容器儲存 Link to heading

// 危險:容器儲存物件本身
std::vector<Animal> animals;

Dog dog;
dog.name = "Buddy";
dog.breed = "Golden Retriever";

animals.push_back(dog);  // 切割!breed 資料遺失
animals[0].makeSound();  // 輸出: Some generic animal sound

場景 3:賦值操作 Link to heading

Dog myDog;
Animal animal;

animal = myDog;  // 切割!只賦值 Animal 部分

場景 4:函式回傳值 Link to heading

// 危險:回傳值會被切割
Animal createDog() {
    Dog dog;
    dog.name = "Max";
    dog.breed = "Labrador";
    return dog;  // 回傳時發生切割!
}

int main() {
    Animal animal = createDog();
    animal.makeSound();  // 輸出: Some generic animal sound
}

正確做法:避免切割 Link to heading

方法 1:使用參考(Reference) Link to heading

// 正確:使用參考,不會切割
void processAnimal(const Animal& animal) {  // pass-by-reference-to-const
    animal.makeSound();  // 會呼叫實際物件的 makeSound()(多型正常)
}

int main() {
    Dog myDog;
    processAnimal(myDog);  // 輸出: Woof! Woof!(多型運作正常)
}

為什麼參考不會切割?

  • 參考只是別名,指向原始的完整物件
  • 不會產生新的物件副本
  • 保留完整的多型資訊(vptr 指向正確的 vtable)

方法 2:使用指標(Pointer) Link to heading

// 正確:使用指標,不會切割
void processAnimal(const Animal* animal) {
    if (animal) {
        animal->makeSound();  // 多型正常運作
    }
}

int main() {
    Dog myDog;
    processAnimal(&myDog);  // 輸出: Woof! Woof!
}

方法 3:容器儲存指標 Link to heading

// 正確:儲存指標(使用智慧指標更好)
std::vector<std::unique_ptr<Animal>> animals;

animals.push_back(std::make_unique<Dog>());
animals[0]->name = "Buddy";

// 需要向下轉型才能存取 breed
if (Dog* dog = dynamic_cast<Dog*>(animals[0].get())) {
    dog->breed = "Golden Retriever";
}

animals[0]->makeSound();  // 輸出: Woof! Woof!(多型正常)

方法 4:回傳指標或參考 Link to heading

// 正確:回傳指標
std::unique_ptr<Animal> createDog() {
    auto dog = std::make_unique<Dog>();
    dog->name = "Max";
    // 注意:需要向下轉型才能設定 breed
    static_cast<Dog*>(dog.get())->breed = "Labrador";
    return dog;
}

int main() {
    auto animal = createDog();
    animal->makeSound();  // 輸出: Woof! Woof!
}

為什麼這在 Effective C++ 中很重要? Link to heading

這就是 Effective C++ 建議「以 pass-by-reference-to-const 取代 pass-by-value」 的原因之一:

1. 避免切割問題 Link to heading

// 在 Object-Oriented C++ 子語言中
void processWidget(const Widget& w);  // 推薦!不會切割

2. 保持多型行為 Link to heading

void displayShape(const Shape& shape) {
    shape.draw();  // 會呼叫正確的 draw() 版本
}

Circle circle;
Rectangle rect;

displayShape(circle);  // 呼叫 Circle::draw()
displayShape(rect);    // 呼叫 Rectangle::draw()

3. 避免昂貴的複製操作 Link to heading

class HugeObject {
    std::vector<int> data(1000000);  // 很大的資料
    // ...
};

// 不好:複製 1000000 個 int
void process(HugeObject obj);

// 好:只傳遞參考(一個指標的大小)
void process(const HugeObject& obj);

實務建議 Link to heading

DO(應該做的) Link to heading

  1. 預設使用 const 參考傳遞自定義類別

    void func(const MyClass& obj);
    
  2. 容器儲存多型物件時使用智慧指標

    std::vector<std::unique_ptr<Base>> objects;
    
  3. 理解何時該用指標、參考或值

    • 內建型別(int, double):pass-by-value
    • 小型物件且不需多型:pass-by-value
    • 大型物件或需要多型:pass-by-reference 或 pointer

DON’T(不應該做的) Link to heading

  1. 不要用 pass-by-value 傳遞多型物件

    void bad(Base obj);  // 會切割
    
  2. 不要在容器中直接儲存多型物件

    std::vector<Base> vec;  // 會切割
    
  3. 不要回傳多型物件本身

    Base createDerived();  // 回傳時會切割
    

偵測切割問題 Link to heading

編譯期偵測(C++11 起) Link to heading

可以將基礎類的複製建構子和賦值運算子刪除,強制使用指標或參考:

class Animal {
public:
    // 禁止複製,避免切割
    Animal(const Animal&) = delete;
    Animal& operator=(const Animal&) = delete;

    // 但允許移動(如果需要)
    Animal(Animal&&) = default;
    Animal& operator=(Animal&&) = default;

    virtual void makeSound() const = 0;
    virtual ~Animal() = default;

protected:
    Animal() = default;  // 只能被派生類呼叫
};

現在如果嘗試切割,會得到編譯錯誤:

Dog myDog;
Animal animal = myDog;  // 編譯錯誤!複製建構子被刪除

小結 Link to heading

物件切割的關鍵點:

  1. 發生時機:用 pass-by-value 傳遞派生類物件給基礎類
  2. 後果
    • 派生類特有資料遺失
    • 多型行為失效
    • 可能造成難以察覺的 bug
  3. 解決方法
    • 使用 pass-by-reference-to-const
    • 使用指標(最好是智慧指標)
    • 考慮刪除基礎類的複製建構子
  4. 適用規則
    • 內建型別:pass-by-value 沒問題
    • 自定義類別(尤其是多型):pass-by-reference-to-const