2016-06-08 71 views
0

編輯1:編輯問題以修復在雅克答案中指出的UB(這是對原始問題的有效答案)。部分構建和銷燬層次結構中的兒童課程對象

考慮下面的代碼:

class C 
{ 
protected: 
    C(bool) : c(0) { s = new char[10]; /* init C members... */ } 
    void cleanup() { delete[s]; /* cleanup C members... */ } //EDIT1 
    C()    { /* do nothing, keep C members unchanged */ } 
    // EDIT1: removed dtor: ~C() { /* do nothing, keep C members unchanged */ } 
    // EDIT1: implicitly defined default (trivial) dtor 
    int c; 
    char* s; 
}; 

class Child1 : public C 
{ 
public: 
    Child1(bool) : C(true) { } 
    void cleanup()   { C::cleanup(); } //EDIT1 
    Child1()    { C++; } 
    // EDIT1: removed dtor: ~Child1() { } 
    // EDIT1: implicitly defined default (trivial) dtor 
}; 

class Child2 : public C 
{ 
public: 
    Child2()    { c --; } 
    void cleanup()   { C::cleanup(); } //EDIT1 
    // EDIT1: removed dtor: ~Child2() { } 
    // EDIT1: implicitly defined default (trivial) dtor 
}; 

int main() 
{ 
    char storage[sizeof(Child1)];   // (0) storage for any C child instance 
    C* child = new(&storage) Child1(true); // (1) create in-place Child1 instance and initialize C members 
    //EDIT1: removed: static_cast<Child1*>(child)->~Child1(); // (2) destroy Child1 instance, keeping C members unchanged 
    child = new(&storage) Child2;   // (3) create in-place Child2 instance, keeping C members unchanged, overwritting Child1 members 
    //EDIT1: removed: static_cast<Child2*>(child)->~Child2(); // (4) destroy Child2 instance, keeping C members unchanged 
    child = new(&storage) Child1(true); // (5) create in-place Child1 instance, keeping C members unchanged, overwritting Child2 members 
    //EDIT1: removed: static_cast<Child1*>(child)->~Child1(); // (6) destroy Child1 instance, keeping C members unchanged 
    child->cleanup();      // (7) cleanup Child1 & C members [EDIT1] 
    return 0; 
} 
  • 在線(1),一個Child1實例是 '就地' 使用非默認構造函數Child1(bool)創建。這導致通過非默認的ctor C(bool)初始化父類C成員。
  • 在第(2)行,Child1實例被銷燬。這將調用母類C的驅動程序,它自願實現爲空,以保持C成員不變。 [EDIT1]
  • 在第(3)行,使用Child2的默認值ctor創建了一個Child2實例。此覆蓋Child1實例 [EDIT1],並調用父類C的默認值ctor,該值自動爲空,以保持C成員不變。

在此步驟中,Child2情況下已經能夠訪問父類C保護成員,保持不變雖然Child1實例已在生產線上進行的重寫操作被破壞(3)。 [EDIT1]

上述模式可以讓我實現我的主要目標:創建和銷燬C類的任何子女的 [EDIT1]的情況下,保持C理事國不變。此外,使用非默認ctor,我有一種方法來初始化成員(例如在第(1)行)。

然而,這種模式有幾個缺點:

  • C成員不能是const或引用,並且必須具有平凡缺省構造函數和析構函數。 (同樣的規則適用於任何子成員。)
  • 清理析構函數不易實現(它可能與初始化非默認ctor C(bool)相同,如果C++支持非默認的dtor,但遺憾的是C++沒有。
  • C成員不能是const或參考。[EDIT1]
  • CC類的父,和類C成員必須具有顯式定義的默認構造函數實現爲空(即瑣碎狀)[EDIT1]
  • class C必須有一個微不足道的。[EDIT1]

我的問題

  • 是上述定義的操作描述的模式[EDIT1]
  • 是否有任何其他的方式來達到同樣的目標( [就地? ] [EDIT1]創建並銷燬 [EDIT1]父類的子類實例C,保留父類C成員不變)沒有缺陷 (特別是第一個) [EDIT1]上面列出的?

理想的情況下,如果我有一個方法,以防止在孩子建造/銷燬過程被調用父類的構造函數C和析構函數,這將是完美的。 [EDIT1]

注1:在實際應用中,C類可能相當大,建設 /銷燬 [EDIT1]的C孩子已經集中出現;就地建設旨在優化這些操作的性能。

注2 [EDIT1]:類C中的微不足道的小孩,在錯誤地調用析構函數的情況下,兒童需要防止未定義的行爲;根據C++標準的§3.8/ 1,當調用析構函數時,具有微不足道的對象的生命期不會結束。

回答

1

你在做什麼是未定義的行爲。

要有一個格式良好的程序,您只能通過正確的類型銷燬一個對象。銷燬後,只能將其存儲空間作爲未初始化的緩衝區訪問。重新創建時,不會保證變量的狀態,也不能保證它們共享之前的狀態。

如果你需要這種行爲,你可以實現一個手動繼承方案,比如C程序員想要一個OO-heiarchy時使用什麼。

這允許獨立於數據的OO標識存儲狀態數據,並允許您即時更改對象的OO標識。

這裏是一個玩具例子:

struct Base_vtable { 
    void(*display)(Base const*); 
}; 
struct Base { 
    static init_vtable(Base_vtable* vtable) { 
    vtable->display = display_raw; 
    } 
    static Base_vtable make_vtable() { 
    Base_vtable vtable; 
    init_vtable(&vtable); 
    return vtable; 
    } 
    static Base_vtable const* get_vtable() { 
    static const auto vtable = make_vtable(); 
    return &vtable; 
    } 
    Base_vtable const* vtable_data = nullptr; 
    Base_vtable const* vtable() const { return vtable_data; } 
    std::array<char, 1000*1000> big_buffer; 
    std::string name; 
    static void display_raw(Base const* self) { 
    std::cout << self->name; 
    } 
    void display() { 
    vtable()->display(this); 
    } 
    static void ctor(Base* self) { 
    self->vtable_data = get_vtable(); 
    } 
    static void dtor(Base* self) { 
    } 
}; 
struct Derived_vtable:Base_vtable { 
    int(*sum)(Derived const*); 
}; 

struct Derived:Base { 
    Derived_vtable const* vtable() { 
    return static_cast<Derived_vtable const*>(vtable_data); 
    } 
    static void init_vtable(Derived_vtable* vtable) { 
    vtable->print = display_raw; 
    vtable->sum = sum_raw; 
    } 
    static Derived_vtable make_vtable() { 
    Derived_vtable d; 
    init_vtable(&d); 
    return d; 
    } 
    static Derived_vtable const* get_vtable() { 
    static const Derived_vtable vtable = make_vtable(); 
    return &vtable; 
    } 
    static int sum_raw(Derived const* self) { 
    int r = 0; 
    for (auto&& c:big_buffer) 
     r+=c; 
    return r; 
    } 
    static void display_raw(Derived const* self) { 
    std::cout << "Derived: "; 
    Base::display_raw(self); 
    } 
    int sum() const { 
    return vtable()->sum(this); 
    } 
    static void ctor(Derived* self) { 
    Base::ctor(self); 
    self->vtable_data = get_vtable(); 
    } 
    static void dtor(Derived* self) { 
    Base::dtor(self); 
    } 
}; 

當你想使用默認的C++面向對象的系統,這非常類似於C++做。除了現在我們有細粒度的控制,我們可以改變我們各種各樣的工具。

我可以將我的狀態與我的虛擬類型分開,允許Derived擁有多個不同的vtables,並在我需要時改變其行爲。這些國家之間的過渡可以做我想做的任何事情。

解決方案的問題在於,允許編譯器使用被破壞的對象的狀態來達到它所選擇的任何目的 - 它可以將它用作交換寄存器的空間。它可以假定存儲在被破壞的結構中的對象是懸掛指針,證明一個指針爲null或指向該存儲,確定我們將解引用它(如果不爲null)並且行爲(如果取消引用的是UB),然後正確地優化您的代碼爲知道指針必須爲空並且不檢查它,免去死代碼分支。

一旦你深入研究了未定義的行爲,你將被迫在未來的每一次編譯器迭代中維護你的代碼,這可能會破壞你的代碼,通過在標準下完全合法的做法。這是一個非常沉重的負載,除非你正在編寫丟棄代碼。

+0

感謝您的回答,以及您在我的計劃中指出的UB。我更新了我的問題(和我的計劃)以擺脫UB。您提出的作爲替代解決方案的「手動繼承方案」看起來很棒,但實施和使用有點棘手;儘管如此,我會更深入地研究它。非常感謝。 – shrike

1

經過對標準的詳盡閱讀後,我可以回答我自己問題的第一部分。我提議的第一個方案(原始問題)是UB,因爲調用對象的析構函數會終止它的生命週期,除非析構函數是微不足道的。標準狀態的§3.8/ 1的是:

T類型的對象的生存期結束時:

- 如果T是具有一個非平凡的析構函數(12.4),析構函數的類類型呼叫開始,或

- 對象佔用的存儲被重用或釋放。

在我提出的更新方案(EDIT1)中,對象的析構函數根本不被調用,而是用一個新對象覆蓋。我雖然這將擺脫UB,但是該標準的§3.8/ 1清楚地表明,當其析構函數被稱爲或者如果它佔用的存儲被重用時,對象的生命週期結束,那正是所做的在覆蓋。 (具體而言,這利用指針UB的小孩。)

然後,我提出的更新方案與第一方案一樣是UB。

關於我的問題的第二部分,Yakk提供了一個有效的解決方案。