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 per std::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 und TFrame .

    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 und T 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 relevanten TObject -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 von TObject.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 nur InitInstance 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 dann InitInstance und CleanupInstance nicht aufgerufen würden. Er könnte prinzipiell schon Code erzeugen, der direkt InitInstance und CleanupInstance anstelle von NewInstance und FreeInstance aufruft; ich vermute aber, daß er das nicht tut. (Das wäre für eine Klasse, die NewInstance und FreeInstance ü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<> und make_unique<>() sollten gehen.



  • Danke audacia, informativ wie immer 👍


Anmelden zum Antworten