Frage an unsere Standardophilen
-
Hallo,
ich habe folgendes Programm - leider etwas länglich, aber notwendig um das Problem zu reproduzieren, aber es wäre trotzdem schön wenn ihr mal drüber schaut

Main.cpp
#include "A.h" int main() { A a; return 0; }A.h
#ifndef AH #define AH #include <memory> class AImpl; class A { public: A(); A( const A&); A& operator=( const A&); ~A(); private: std::auto_ptr<AImpl> impl_; }; #endifA.cpp
#include "A.h" #include <algorithm> #include <iostream> #include "B.h" using namespace std; A::A() : impl_( new AImpl) { cout << "A create" << endl; } A::A( const A& other) : impl_( new AImpl( *other.impl_.get())) { cout << "A copy" << endl; } A& A::operator=( const A& other) { cout << "A assign" << endl; A tmp( other); swap( impl_, tmp.impl_); return *this; } A::~A() { cout << "A destroy" << endl; }B.h
#ifndef BH #define BH #include <memory> class BImpl; class AImpl { public: AImpl(); AImpl( const AImpl&); AImpl& operator=( const AImpl&); ~AImpl(); private: std::auto_ptr<BImpl> impl_; }; #endifB.cpp
#include "B.h" #include <algorithm> #include <iostream> using namespace std; class BImpl { public: BImpl() { cout << "BImpl create" << endl; } BImpl( const BImpl&) { cout << "BImpl copy" << endl; } ~BImpl() { cout << "BImpl destroy" << endl; } BImpl& operator=( const BImpl&) { cout << "BImpl assign" << endl; return *this; } }; AImpl::AImpl() : impl_( new BImpl) { cout << "AImpl create" << endl; } AImpl::AImpl( const AImpl& other) : impl_( new BImpl( *other.impl_.get())) { cout << "AImpl copy" << endl; } AImpl& AImpl::operator=( const AImpl& other) { cout << "AImpl assign" << endl; AImpl tmp( other); swap( impl_, tmp.impl_); return *this; } AImpl::~AImpl() { cout << "AImpl destroy" << endl; }Welchen Output sollte das Programm eurer Meinung nach produzieren, bzw. welchen produziert es (dann wäre Compiler inkl. Version interessant)?
Welche Freiheiten hat der Compiler, Kopien etc zu entfernen, zu inlinenen usw.?
Bitte keine Diskussion über Sinn und Unsinn
Hintergrund ist jener: Der Borland C++ Compiler 2007 spuckt folgendes aus:BImpl create
AImpl create
A create
A destroy
AImpl destroyBImpl wird nicht zerstört. Im Singlestep wird zwar der Destruktor von auto_ptr<BImpl> angelaufen und auch ein delete gemacht, aber das ruft den Destruktor von BImpl nicht auf, was wiederum eigentlich nur heißen kann, dass die Definition von BImpl an dieser Stelle nicht bekannt wäre.
Verstehe ich nicht... kann eigentlich nur heißen, nicht standardkonform oder Compilerbug
-
So, ich habe gerade auch mal dieses hier vom Herrn Sutter wiedergefunden:
http://www.gotw.ca/publications/using_auto_ptr_effectively.htmDa implementiert er ein Pimpl genauso - behauptet sogar, dass man den Destruktor weglassen kann, was ich wiederum bezweifele, da wenn der Destruktor nun implizit inline generiert wird, der auto_ptr die Definition des PImpls nicht kennen kann.
Da aber in obigem Beispiel der Destruktor ja eh im .cpp liegt, sollte selbst dieses potenzielle Problem umgangen sein. Ich habe es übrigens sogar schon ausprobiert, den Destruktor virtuell zu machen etc.
Inlining ist vollständig deaktiviert.
-
7H3 N4C3R schrieb:
So, ich habe gerade auch mal dieses hier vom Herrn Sutter wiedergefunden:
http://www.gotw.ca/publications/using_auto_ptr_effectively.htmDa implementiert er ein Pimpl genauso - behauptet sogar, dass man den Destruktor weglassen kann, was ich wiederum bezweifele, da wenn der Destruktor nun implizit inline generiert wird, der auto_ptr die Definition des PImpls nicht kennen kann.
Da aber in obigem Beispiel der Destruktor ja eh im .cpp liegt, sollte selbst dieses potenzielle Problem umgangen sein. Ich habe es übrigens sogar schon ausprobiert, den Destruktor virtuell zu machen etc.
Inlining ist vollständig deaktiviert.Mein g++ heißt:
gcc version 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)Und dein Programm sagt dann:
$ ./AB BImpl create AImpl create A create A destroy AImpl destroy BImpl destroyUnd so sollte es IMHO wohl auch sein.
-
Das ganze ist undefiniert wegen 17.4.3.6/2 letzter Fall
In particular, the effects are undefined in the following cases:
...
— if an incomplete type (3.9) is used as a template argument when instantiating a template component.Die Verwendung eines anderen Smartpointers, der ausdrücklich auch bei der Zerstörung korrekt mit unvollständigen Templateargumenten umgehen kann (etwa: shared_ptr; z.B. nicht: scoped_ptr), kann dieses Problem lösen. Allgemeingültiger dürfte allerdings die explizite Deklaration und Definition (nicht inline) des Destruktors sein. Bei pimpl wird man in der Regel ja sowieso die großen Drei definieren müssen. Wenn man das tut, ist an dieser Stelle ein nackter Zeiger ausnahmsweise mal sinnvoller. Persönlich denke ich ja - das wird aber sicher auf Widerspruch treffen - dass void* der beste Typ für pimpl ist, nur damit wird die Implementation vollständig entkoppelt. Bei Impl* ist Impl zumindest immer noch eine Klasse mit external linkage, man kann sie also z.B. nicht mal eben umbenennen oder in einen privaten Namensraum verschieben etc.
-
Bin ich blind, oder hat der OP _alle_ Destruktoren dekalriert !?
-
bladerunner10 schrieb:
Bin ich blind, oder hat der OP _alle_ Destruktoren dekalriert !?
Nicht bevor die jeweiligen auto_ptr-Objekte definiert wurden. Danach ist es zu spät.
-
camper schrieb:
bladerunner10 schrieb:
Bin ich blind, oder hat der OP _alle_ Destruktoren dekalriert !?
Nicht bevor die jeweiligen auto_ptr-Objekte definiert wurden. Danach ist es zu spät.
Die Destruktoren sind doch vor den auto_ptr's deklariert, und sind alle ausdrücklich deklariert und nicht inline. Oder verstehe ich dich falsch? (oder meinst du die Destruktoren der Impls in Bezug auf den auto_ptr in der besitzenden Klasse?)
Kann hier evtl. noch der Wechsel der Sichtbarkeit in die Quere? "Reihenfolgen" waren doch IIRC nur innerhalb eines Sichtbarkeitsbereichs festgelegt.
Würde in Folgerung dann auch bedeuten, dass das Beispiel von Sutter undefiniertes Verhalten ist?
-
Betrachte zum Beispiel (die anderen analog)
#ifndef AH #define AH #include <memory> class AImpl; class A { public: A(); A( const A&); A& operator=( const A&); ~A(); private: std::auto_ptr<AImpl> impl_; }; #endifAImpl ist hier bei der Definition von A::impl_ ein unvollständiger Typ. Da dies eine Definition ist, führt es zur Instantiierung von std::auto_ptr<AImpl> an dieser Stelle. Das ist, wie ausgeführt, undefiniert. Die Tatsache, dass der Destruktor von A hier explizit deklariert wird, dürfte in vielen Fällen dafür sorgen, dass das Programm in der Praxis doch noch das tut, was wir naiv erwarten. Andererseits halte ich wenig von Konstruktionen, die so fragil sind, dass sie durch bloße inline-Definition einer Funktion (~A) im Header (verletzt dann die ODR) kaputt gehen würden. Und ja, das heißt auch, dass Sutter hier irrt.
-
Okay, danke, ist mir dann klar denke ich. Ziemlich dreckige Falle

Ich werde mir wohl mal die Implementierung von shared_ptr zu Gemüte führen, das interessiert mich mal wie die das mit unvollständigen Typinformationen hinbekommen.
-
Hallo,
ich bin mir der derzeitigen Antwort noch nicht ganz zufrieden.
Okay, std::auto_ptr hat gemäß C++ Standard per Definition ein undefiniertes Verhalten für unvollständige Typen.
Nach etwas weiterführender Lektüre in comp.lang.c++.moderated riecht hier immernoch alles nach Compilerbug.
Ersetzen wir in obigem Beispiel mal std::auto_ptr durch eine eigene Klasse (nur als sinnhaftes Beispiel, nicht als voll compilierfähiges und einsatzfähiges Beispiel):
template <class T> class MyAutoPtr : public noncopyable { private: T* t_; public: explicit MyAutoPtr( T* t) : t_( t) {} ~MyAutoPtr() { delete t_; } }Und dann folgende Klasse:
A.h
#include "MyAutoPtr.h" class A : public noncopyable { public: A(); ~A() private: class AImpl; MyAutoPtr<AImpl> impl_; }A.cpp
#include "A.h" class A::AImpl {}; A::A() : impl_( new AImpl) A::~A() {}Dieser Code sollte doch konform sein, oder?
Der Destruktor von MyAutpPtr<AImpl> muss doch jetzt in A::~A im Cpp-File instanziiert werden, oder nicht (und damit eben an einer Stelle, an der AImpl kein unvollständiger Typ mehr ist)?
Da ich weiß (durch nachschauen in der Implementierung), dass std::auto_ptr für meine ganz konkrete Implementierung mit einem T* t als Member und einem delete t im Destruktor implementiert ist (bzw void* mit cast im Destruktor und damit gleichwertig zu meinem Beispiel sein sollte), sollte hier ein Fehler im Compiler vorliegen? Bevor sich jemand beschwert - ich könnte auch einfach die Implementierung aus den Headern rauskopieren, umbenennen und dann benutzen - damit wäre die Restriktion des Standards "egal" und nur noch der oben erwähnte Code inkraft.
Über Angaben zu entsprechenden Passagen im Standard für ein für und wider würde ich mich freuen.
Grüße
-
7H3 N4C3R schrieb:
Dieser Code sollte doch konform sein, oder?
Mir fällt nichts gegenteiliges auf.
7H3 N4C3R schrieb:
Der Destruktor von MyAutpPtr<AImpl> muss doch jetzt in A::~A im Cpp-File instanziiert werden, oder nicht (und damit eben an einer Stelle, an der AImpl kein unvollständiger Typ mehr ist)?
Ja. Ein Problem könnte allerdings im Prinzip entstehen, falls der Destruktor von MyAutpPtr<AImpl> an irgendeiner anderen Stelle des Programmes (an der AImpl nicht vollständig definiert ist) instantiiert wird (aus welchem Grunde auch immer). Dann liegt ggf. eine ODR-Verletzung vor und wird wissen nicht, welche Version im Destruktor von A aufgerufen werden wird. Am Besten führt man immer eine Prüfung hinsichtlich der vollständigen Definition vor dem Löschen aus. Boosts Smartpointer z.B. benutzen in der Regel checked_delete - ein Versuch, die Destruktoren zu instantiieren, wenn das Templateargument unvollständig ist, führt also direkt zu einem Fehler beim Compilieren.
-
Danke, das mit der ODR-Verletzung ist mir jetzt auch nochmal wirklich klar geworden. Ich frage mich, warum der Linker trotz aller Warnungen und Verbose-Flag im ursprünglichen Beispiel nicht gemeckert hat.
Edit: Okay, kann er eigentlich nicht, bzw. allenfalls möglich, wenn kein inlining stattgefunden hat und er die weak symbols inhaltlich vergleichen würde. Das ist wohl verzeihbar...
camper schrieb:
Ein Problem könnte allerdings im Prinzip entstehen, falls der Destruktor von MyAutpPtr<AImpl> an irgendeiner anderen Stelle des Programmes (an der AImpl nicht vollständig definiert ist) instantiiert wird (aus welchem Grunde auch immer).
Was für Gründe können das denn sein? Was ich mich frage ist, ob du damit völlig legale Gründe meinst wo das passieren kann (also so, wie der Code zuletzt in meinem Beispiel stand), oder auf unsaubere Benutzung des MyAutoPtr<AImpl>-Konstrukt abzielst. Letzteres wäre mir klar, aber wenn dir zu ersterem etwas einfällt, wäre ich neugierig.
-
Was du tust entspricht zusammengefasst, ohne die ganzen Klassen, dem hier:
class Impl; Impl* ptr = 0; delete ptr; // destructor ~Impl() is undefined => undefined behaviour.Du versuchst eine nicht definierte Klasse zu 'deleten'. Das führt - wie von camper erwähnt - zu undefiniertem Verhalten. Beim MSVC kann es zB dazu kommen, dass der Destruktor gar nicht aufgerufen wird (sowas in der Art wird bei dir passiert sein).
Der MSVC warnt:warning C4150: deletion of pointer to incomplete type 'main::Impl'; no destructor calledDas ist kein Compilerbug, sondern ein ganz normaler Programmiererbug.
Gruß
Don06
-
Don06 schrieb:
Was du tust entspricht zusammengefasst, ohne die ganzen Klassen, dem hier:
class Impl; Impl* ptr = 0; delete ptr; // destructor ~Impl() is undefined => undefined behaviour.Du versuchst eine nicht definierte Klasse zu 'deleten'. Das führt - wie von camper erwähnt - zu undefiniertem Verhalten.
Nur dann, wenn der Destruktor von Impl nicht trivial ist (das wird für solche Impl-Klassen wohl in der Regel zutreffen) oder der delete-Operator für diese Klasse überladen wird.
-
Don06 schrieb:
Das ist kein Compilerbug, sondern ein ganz normaler Programmiererbug.
Hast du den gesamten Thread gelesen?

Es geht mir dadrum, wenn der explizit geschrieben ist und im .cpp-File steht und somit der Code eigentlich konform sein sollte, aber im RAD-Studio 2007 anscheinend trotzalledem nicht aufgerufen wird.