Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> C++

การเรียก Virtual Functions ภายใน Constructor/Destructor ใน C++: อันตรายและข้อควรระวัง

การเรียก virtual function ภายใน constructor หรือ destructor เป็น **อันตรายและควรหลีกเลี่ยง** เพราะพฤติกรรมไม่ได้เป็นไปตามที่คาดหวัง (ไม่ได้เรียก derived version)

เหตุผล: Construction/Destruction Order

  1. Construction: Base constructor รัน **ก่อน** Derived constructor
  2. Destruction: Derived destructor รัน **ก่อน** Base destructor
  3. ระหว่าง Base constructor รัน: Object **ยังไม่ใช่** Derived type → vtable ยังเป็นของ Base

ตัวอย่างปัญหาใน Constructor

#include <iostream>

using namespace std;

class Base {
public:
    Base() {
        cout << "Base constructor\n";
        init();  // ❌ เรียก virtual function ใน constructor
    }
    
    virtual void init() {
        cout << "Base::init()\n";
    }
    
    virtual ~Base() = default;
};

class Derived : public Base {
    int value;
public:
    Derived(int v) : value(v) {
        cout << "Derived constructor\n";
    }
    
    void init() override {
        cout << "Derived::init(), value = " << value << endl;
        // value ยังไม่ได้ initialize! (Base constructor รันก่อน)
    }
};

int main() {
    cout << "Creating Derived(42):\n";
    Derived d(42);
    cout << "Done\n";
}

ผลลัพธ์ (แสดงปัญหา)

Creating Derived(42):
Base constructor
Base::init()      // ← เรียก Base version! ไม่ใช่ Derived
Derived constructor
Done

ปัญหาเพิ่มเติม: ใช้ member ก่อน initialize

ในตัวอย่างข้างต้น Derived::init() เข้าถึง value แต่ value ยังไม่ถูก initialize (Derived constructor ยังไม่รัน) → **Undefined Behavior**

ตัวอย่างใน Destructor

class Base {
public:
    virtual ~Base() {
        cleanup();  // ❌ เรียก virtual ใน destructor
    }
    virtual void cleanup() { cout << "Base cleanup\n"; }
};

class Derived : public Base {
    Resource* res;
public:
    Derived() : res(new Resource) {}
    ~Derived() override {
        delete res;  // Derived cleanup ก่อน
        res = nullptr;
    }
    void cleanup() override {
        cout << "Derived cleanup\n";
        // res ถูกลบแล้ว! (Base destructor รันหลัง Derived)
    }
};

ผลลัพธ์ Destructor

Derived cleanup   // จาก ~Derived() เรียก cleanup() ผ่าน virtual? ไม่ - มันเรียก Base::cleanup()!
Base cleanup

จริงๆ แล้วใน destructor ของ Base vtable 已經กลายเป็น Base แล้ว (Derived part ถูกทำลายไปแล้ว) ดังนั้นเรียก Base::cleanup()

วิธีแก้ไขที่ปลอดภัย

1. Non-Virtual Interface (NVI) Pattern

class Base {
public:
    Base() { initialize(); }  // เรียก non-virtual
    
    void initialize() {       // Non-virtual public interface
        // Common initialization
        doInitialize();       // เรียก private virtual
    }
    
    virtual ~Base() {
        cleanup();
    }
    
    void cleanup() {          // Non-virtual
        doCleanup();
    }

private:
    virtual void doInitialize() { cout << "Base init\n"; }
    virtual void doCleanup() { cout << "Base cleanup\n"; }
};

class Derived : public Base {
private:
    void doInitialize() override { cout << "Derived init\n"; }
    void doCleanup() override { cout << "Derived cleanup\n"; }
};

2. Two-Phase Initialization

class Base {
public:
    Base() {}  // ไม่เรียก virtual
    
    // เรียกหลัง construction เสร็จ
    virtual bool init() { return true; }
    
    // Factory method
    static unique_ptr<Base> create() {
        auto ptr = make_unique<Derived>();
        if (ptr->init()) return ptr;
        return nullptr;
    }
};

3. ใช้ CRTP (Curiously Recurring Template Pattern)

template <typename Derived>
class Base {
public:
    Base() {
        static_cast<Derived*>(this)->init();  // Static dispatch
    }
};

class Derived : public Base<Derived> {
public:
    void init() { cout << "Derived init\n"; }
};

Best Practices

  • ❌ **อย่า** เรียก virtual functions ใน constructor/destructor
  • ✅ ใช้ **NVI pattern** (public non-virtual เรียก private virtual)
  • ✅ ใช้ **Two-phase initialization** (init() แยกต่างหาก)
  • ✅ ใช้ **Factory functions** สำหรับ object creation
  • ✅ ใช้ **CRTP** สำหรับ static polymorphism

ข้อยกเว้น: Pure Virtual Destructor

Pure virtual destructor **ต้องมี body** และถูกเรียกปกติใน destruction chain (ไม่ใช่ virtual call ปกติ)