initialization in C++03
-
folgendes Problem:
class C steht in "has-a"-Beziehung mit class A und class B, public inheritance kommt nicht in Frage, private inheritance möchte ich vermeiden.
class A{ ... }; // ctor might throw class B{ ... }; // ctor might throw class C{ public: C(){ try { A_ = new A(); } catch(...){ ... } try { B_1 = new B(); } catch(...){ delete A_; } try { B_2 = new B(); } catch(...){ delete B_1; delete A_; } } private: A* A_; B* B_1, B_2; }Mir gefallen die delete-Ketten in den Handlern nicht; selbst, wenn man mit goto code duplication vermeiden könnte.
Gibt es bessere Alternativen, z.B. per initializer list (Frage exception safety)? Oder Resource handler, ohne daß ich für B, C copy-ctor implementieren muß (den haben sie nämlich nicht)?
Randbedingungen:
C++03, keine smart pointers, "has-a"-Beziehung (nicht "is-a")
-
Warum keine Smart Pointer? Oder warum überhaupt Pointer?
class C { public: C() { } private: A a_; B B_1; B B_2; };So vermeidest Du auch die Notwendigkeit einen Copy-Konstruktor und Assignment-Operator implementieren zu müssen.
-
tntnet schrieb:
Warum keine Smart Pointer? Oder warum überhaupt Pointer?
keine Smartpointer und legacy C++ ist Bedingung. Sonst wäre es ja einfach. Und warum überhaupt Pointer? Weil der ctor von A, B einen parent-Zeiger braucht:
A::A(C* parent) : C_(parent){ ... }
B::B(C* parent) : C_(parent) { ... }damit fällt in-class init. a la "class C { ... private: A a_; ...}" schon mal weg.
-
Geht doch mit Initializer Lists:
class C { public: C() : a_(this), B_1(this), B_2(this) { } private: A a_; B B_1; B B_2; };Oder brauchen die Konstruktoren von A oder B schon ein vollständiges C Objekt?
klassenmethode schrieb:
keine Smartpointer und legacy C++ ist Bedingung
Legacy C++ kann ich ja noch verstehen. Ist das irgendeine Programmieraufgabe oder wieso sind Smart Pointer ausgeschlossen? Man kann sich ja auch in C++03 einfach seinen unique_ptr Klon ohne move und andere tolle Tricks schnell selbst bauen. Oder was aus boost nehmen.
-
sebi707 schrieb:
Oder brauchen die Konstruktoren von A oder B schon ein vollständiges C Objekt?
Wäre auch kein Hinderungsgrund. Wenn A oder B deklariert wird, reicht eine forward deklaration von C. Wird der Konstruktor implementiert, dann kann bereits C kennen
class C; // forward deklaration class A { public: explicit A(const C& c); // hier braucht er definitiv noch kein C }; class C { public: C() : a_(*this) { } private: A a_; }; A::A(const C& c) { // und hier kennt er das vollständige C Objekt }Zeilen 1-7 gehören normalerweise in "A.h", 8-17 in "C.cpp" und 18-21 in "A.cpp"
Ach - C-Objekt klingt komisch. Ist doch ein C++-Objekt
. "Objekt vom Typ C" wäre besser. Aber was solls.
-
tntnet schrieb:
Wäre auch kein Hinderungsgrund. Wenn A oder B deklariert wird, reicht eine forward deklaration von C.
Sorry, ich meinte vollständig im Sinne von vollständig konstruiert. Wenn der Konstruktor von A eine bereits erfolgte und möglicherweise komplizierte Initialisierung des C Objekts voraussetzt dann möchte man das A Objekt vielleicht erst am Ende des Konstruktors von C aufrufen. Das ist dann nur schwierig mit Initializer Lists zu realisieren.
-
initializer list geht nicht, weil wie schon oben geschrieben:
class A{ ... }; // ctor might throw class B{ ... }; // ctor might throwDaß der ctor von A hingegen ein vollständig initialized C-Objekt benötigt, ist nicht verlangt. Es reicht die Adresse.
-
Und? Lass die doch ne Exception werfen. Die Member werden schon richtig aufgeräumt.
-
klassenmethode schrieb:
Mir gefallen die delete-Ketten in den Handlern nicht;
Mir gefallen die ganzen try/catch Blöcke nicht.
klassenmethode schrieb:
Gibt es bessere Alternativen [...]
Ja, Smartpointer
klassenmethode schrieb:
Randbedingungen:
[...] keine smart pointers [...]Mit Verlaub, aber das sind ziemlich bescheuerte Bedingungen, darf man fragen, woher die genau kommen? Wieso überhaupt C++ verwenden?
-
@sebi707: kannst du mal zeigen, wie du eine exception aus einer initializer list fängst, die mit
C::C() : A_(new A(this)), B_1(new B(this)), B_2(new B(this)) {}gebaut ist?
C::C() : A_(A(this)), B_1(B(this)), B_2(B(this)) {}will ich ja nicht, weil A und B keinen copy-ctor haben (wäre zu aufwendig)
-
klassenmethode schrieb:
@sebi707: kannst du mal zeigen, wie du eine exception aus einer initializer list fängst, die mit
C::C() : A_(new A(this)), B_1(new B(this)), B_2(new B(this)) {}gebaut ist?
Das ist nicht, was sebi meint, es sei denn A_, B_1 (und B_2) sind Smartpointer. Falls es Smartpointer sind, gibt es keinen Grund zu fangen.
klassenmethode schrieb:
C::C() : A_(A(this)), B_1(B(this)), B_2(B(this)) {}will ich ja nicht, weil A und B keinen copy-ctor haben (wäre zu aufwendig)
dito. Niemand verlangt, dass kopiert werden soll.
C::C() : A_(this), B_1(this), B_2(this) {}ist gut genug.
Wenn neben dem Aufräumen noch etwas anderes im Handler getan werden muss: Stichwort function-try-block
-
klassenmethode schrieb:
@sebi707: kannst du mal zeigen, wie du eine exception aus einer initializer list fängst, die mit
C::C() : A_(new A(this)), B_1(new B(this)), B_2(new B(this)) {}gebaut ist?
Mit einem function-try-block. Das hilft dir aber leider nicht viel, weil du nicht weißt, wo genau die exception flog und welche Member nun aufgeräumt werden müssen. Die einzige, mir bekannte, ordentliche Lösung für all diese Probleme, ist RAII, aber das darf ja aus irgendeinem Grund offenbar nicht verwendet werden...
-
Das hilft dir aber leider nicht viel, weil du nicht weißt, wo genau die exception flog und welche Member nun aufgeräumt werden müssen.
Im function-try-block wären alle Member schon zerstört.
-
Arcoth schrieb:
Das hilft dir aber leider nicht viel, weil du nicht weißt, wo genau die exception flog und welche Member nun aufgeräumt werden müssen.
Im function-try-block wären alle Member schon zerstört.
Stimmt. Der Punkt ist, dass alles per new allokierte Memory hier im Falle einer Exception zwangsweise leaked, so lange man keine Smartpointer einsetzt...

-
Unabhängig davon, dass ich die Lösung ohne Zeiger auch am besten finde:
Der Originalcode lässt sich mit relativ einfachen Mitteln etwas aufhübschen.class C{ public: C() : A_(0), B_1(0), B_2(0) { try{ A_=new A(); B_1=new B(); B_2=new B(); } catch(...){ dealloc(); throw; } } private: void dealloc() { delete A_; delete B_1; delete B_2; } ... A* A_; B* B_1; B* B_2; };
-
dot schrieb:
Arcoth schrieb:
Das hilft dir aber leider nicht viel, weil du nicht weißt, wo genau die exception flog und welche Member nun aufgeräumt werden müssen.
Im function-try-block wären alle Member schon zerstört.
Stimmt. Der Punkt ist, dass alles per new allokierte Memory hier im Falle einer Exception zwangsweise leaked, so lange man keine Smartpointer einsetzt...

Du hast ihn womöglich dazu angeregt, nachzudenken, wie er diese wohl im function-try-block zerstören sollte. Ich wollte nur sicherstellen, dass er diese Idee nicht verfolgt.
Eigentlich sollte ein Artikel zu RAII rauskommen, der wirklich bitter nötig scheint, aber jemand hatte ja über die Feiertage keine Zeit…

-
Wenn
A::AundB::BdenC-Zeiger nicht gleich verwenden (=kein vollständig initialisiertesC-Objekt benötigen), dann die Lösung ohne ZeigerC::C() : A_(this), B_1(this), B_2(this) {}Ansonsten Smart-Pointer.
Oder wenigstens eine einfache, kleine Guard-Klasse:template <class T> struct DeleteGuard { DeleteGuard() : p(0) {} ~DeleteGuard() { delete p; } T* p; private: DeleteGuard(DeleteGuard const&); DeleteGuard& operator = (DeleteGuard const&); }; class C { public: C() { A_.p = new ...; B_1.p = new ...; B_2.p = new ...; } private: DeleteGuard<A> A_; DeleteGuard<B> B_1; DeleteGuard<B> B_2; };Oder
Cin zwei Klassen splitten:CBaseundC, wobeiCvonCBaseabgeleitet ist.
AundBsind dann Member vonC, bekommen aber nur einenCBase-Zeiger. Zu dem Zeitpunkt woAundBkonstruiert werden ist CBase bereits vollständig initialisiert,AundBkönnen also problemlos darauf zugreifen.
-
ich habe noch mal drüber nachgedacht - was Sebi707 vorgeschlagen hat, scheint mir die beste Lösung
class C; class A { public: A(C &parent); virtual ~A(); private: C &C_; }; class B { public: B(C &parent); virtual ~B(); private: C &C_; }; class C { public: C(); virtual ~C(); private: A A_; B B1_; B B2_; }; C::C() : A_(*this), B1_(*this), B2_(*this){} C::~C(){} A::A(C &parent) : C_(parent){} A::~A(){} B::B(C &parent) : C_(parent){ throw exception(); } B::~B(){} int main(void){ try { C c; } catch(...){ /* ... */ } }hübsch - die delete-Ketten sind weg.
-
Wieso die ganzen virtuellen Destruktoren? Ich finds auch etwas merkwürdig, dass diese Klassen alle eine Referenz auf das Objekt, dessen Teil sie sind, brauchen...
-
klassenmethode schrieb:
i
class C; class A { public: A(C &parent); virtual ~A(); private: C &C_; }; (...)Also hier würde ich wirklich keine Referenz verwenden, sondern einen Zeiger.
Wenn du das Ändern des Zeigers verhindern willst, dann mach die Membervariable einfachconst.Und der virtuelle Dtor ist mMn. auch unnütz. Kann mir grad keine Situation vorstellen wo man den brauchen könnte. Bzw. schlimmer als unnütz: er kommuniziert etwas, was so nicht stimmt. Nämlich dass man sinnvoll von A ableiten kann.
Und dann macht es wohl auch keinen Sinn eine implizite Konvertierung
C=>Abzw.C*=>Azu erlauben. Also sollte der Ctorexplicitsein.Also
class C; class A { public: explicit A(C* parent); private: C* const C_; };