TComponent und make_unique/make_shared
-
Hallo,
ich hab den Verdacht, dass VCL-Komponenten und
std::make_unique
bzw.std::make_shared
nicht kompatibel sind. Manchmal bekomme ich Zugriffsverletzungen, wenn ich auf Eigenschaften eines Objekts zugreifen möchte, das ich perstd::make_unique
bzw.std::make_shared
erzeugt habe. Nicht immer, aber immer mal wieder. Ich weiß, das ist ziemlich vage, aber hat jemand von euch ähnliche Erfahrungen gemacht?Beispielcode mit TChartSeries, ähnliche Fehler hatte ich auch schon mit
TForm
undTFrame
.std::shared_ptr<TBarSeries> create_bar_series( TChart* Parent, TColor Color ) { // Fall 1) std::shared_ptr<TBarSeries> Series( new TBarSeries( nullptr ) ); // Fall 2) std::shared_ptr<TBarSeries> Series = make_shared<TBarSeries>( nullptr ); // Zuweisung an ParentChart funktioniert im Fall 1), Zugriffsfehler im Fall 2) Series->ParentChart = Parent; // Series->MultiBar = mbStacked; Series->Marks->Visible= false; Series->Color = Color; Series->BarPen->Visible = false; Series->AutoBarSize = true; return Series; }
-
Ich kenn mich jetzt mit TeeChart nicht näher aus, aber ich nehme mal an, daß du Vorsorge getroffen hast, daß niemand anderes die
TBarSeries
freigibt.make_shared<T>()
ist ein bißchen speziell, weil es nur einmal Speicher anfordert undT
mittels placement new in diesem Speicher konstruiert.Leider sind Delphi-Klassen (= alles, was von
TObject
erbt, auch wenn es in C++ geschrieben ist) auch ein bißchen speziell in bezug auf Konstruktion und Destruktion. Ich habe gerade keine Delphi-RTL zur Hand, um das nachzuprüfen, aber ich habe mir mal anhand der FreePascal-Sourcen die relevantenTObject
-Methoden zusammengesucht:type TObject = class constructor Create; destructor Destroy; virtual; class function NewInstance: TObject; virtual; procedure FreeInstance; virtual; ... procedure Free; class function InitInstance(Instance: Pointer): TObject; procedure CleanupInstance; ... end; constructor TObject.Create; begin end; destructor TObject.Destroy; begin end; class function TObject.NewInstance : TObject; var P: Pointer; begin GetMem(P, InstanceSize); if p <> nil then InitInstance(p); Result := TObject(P); end; procedure TObject.FreeInstance; begin CleanupInstance; FreeMem(Pointer(Self)); end; procedure TObject.Free; begin if Self <> nil then Self.Destroy; { passes True as hidden argument } end; class function TObject.InitInstance(Instance: Pointer): TObject; begin ... { initialize instance to zero, then assign all VMT references } end; procedure TObject.CleanupInstance; begin ... { release all managed fields } end;
Wenn du jetzt eine abgeleitete Klasse
TFoo
definierst und verwendest, erzeugt der Compiler ungefähr folgenden Code:type TFoo = class end; { pseudocode to represent what the compiler generates } function TFoo.Create(Init: Boolean): TFoo; begin if IsClassRef then try Self := InitInstance(TSomething.NewInstance); { actual constructor body is inserted here } inherited Create(False); except Self.Destroy; end else begin { actual constructor body is inserted here; of course in reality the compiler avoids the code duplication } inherited Create(False); end; Result := Self; end; { pseudocode to represent what the compiler generates } procedure TFoo.Destroy(Deallocate: Boolean); begin if Deallocate then Self.BeforeDestruction; { actual destructor body is inserted here } inherited Destroy(False); if Deallocate then FreeInstance; end; procedure UseFoo; var Foo: TFoo; begin Foo := TFoo.Create; { passes True as hidden argument } try { ... } finally Foo.Free; end; end;
Für in C++ definierte Klassen wird der erzeugte Code nochmal komplizierter, weil der Compiler natürlich nicht, wie in Delphi üblich, den Destruktor aufrufen kann, wenn im Konstruktor eine Exception geworfen wird, weil sonst ggf. noch nicht initialisierte Members destruiert werden. Aber das spielt für unsere Betrachtung keine Rolle.
Du siehst, daß Speicher von
TObject.NewInstance
alloziert und vonTObject.FreeInstance
freigegeben wird. Für placement new müßte das natürlich unterbunden werden. Weil beide Methoden virtuell sind, kann man das erreichen, indem man sie in einer Subklasse überschreibt, die Allokation bzw. Freigabe übergeht und nurInitInstance
bzw.CleanupInstance
aufruft. Allerdings ist das immer die Entscheidung desjenigen, der die Subklasse definiert, und nicht die des Aufrufers. Ein vom Aufrufer vorgenommenes placement new ist also nicht vorgesehen.Ich weiß jetzt leider nicht mehr, was der C++Compiler für Code erzeugt, wenn man placement new oder explizite Destruktoraufrufe mit Delphi-Klassen verwendet. Man könnte vermuten, daß er einfach den Konstruktor bzw. Destruktor aufruft und
False
als hidden argument übergibt; aber das wäre nicht korrekt, u.a., weil dannInitInstance
undCleanupInstance
nicht aufgerufen würden. Er könnte prinzipiell schon Code erzeugen, der direktInitInstance
undCleanupInstance
anstelle vonNewInstance
undFreeInstance
aufruft; ich vermute aber, daß er das nicht tut. (Das wäre für eine Klasse, dieNewInstance
undFreeInstance
überschreibt, auch sehr unerwartet.) Warum placement new und expliziter Destruktoraufruf bei Delphi-Klassen dann überhaupt kompiliert werden, ist freilich sehr die Frage.Kurzum: placement new mit Delphi-Klassen geht vermutlich nicht, und deshalb geht auch
make_shared<>()
nicht.shared_ptr<>
,unique_ptr<>
undmake_unique<>()
sollten gehen.
-
Danke audacia, informativ wie immer