[Gelöst] TThread constructor: warum kein "override"?

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

[Gelöst] TThread constructor: warum kein "override"?

Beitrag von Nimral »

[Edit: nach Studium der angebotenen Erklärungen bin ich zum Schluss gekommen, dass die Erklärungen für mich mit meinem derzeitigen Mindset nicht zu verarbeiten sind, und dass ich deswegen mit "ist halt so" leben muss]

Ich dachte, ich hätte die Basics von OOP unter Pascal geschnallt, und habe mich fortan an folgendes Schema gehalten:

- wenn ich möchte, dass ein abgeleitetes Objekt meine Methoden, wozu auch der Constructor zählt, überschreiben kann, *muss* ich die Methoden "virtual" markieren.
- wenn ich in einem abgeleiteten Objekt eine Methode überschreiben will, *muss* ich sie mit "override" markieren, und kann ggf. die überschriebene Methode mit inherited aufrufen.

Heute begegnet mir im Wiki [[Multithreaded_Application_Tutorial]] folgender Code:

Code: Alles auswählen

  Type
    TMyThread = class(TThread)
    private
      fStatusText : string;
      procedure ShowStatus;
    protected
      procedure Execute; override;
    public
      Constructor Create(CreateSuspended : boolean);
    end;
  constructor TMyThread.Create(CreateSuspended : boolean);
  begin
    inherited Create(CreateSuspended);
    FreeOnTerminate := True;
  end;
Beim Constructor kein virtual, kein override, aber inherited. Execute dagegen passt ins Schema. Eigentlich hätte ich erwartet, dass es zumindest eine Compiler-Warnung gibt, ich hätte eine geerbte Methode "verborgen", aber nix dergleichen passiert.

Bin ich auf dem falschen Dampfer?

Armin
Zuletzt geändert von Nimral am Do 20. Jan 2022, 23:24, insgesamt 1-mal geändert.

Benutzeravatar
theo
Beiträge: 10497
Registriert: Mo 11. Sep 2006, 19:01

Re: TThread constructor: warum kein "override"?

Beitrag von theo »

Wie du ja selber feststellst: TObject.Create ist nicht virtual und kann deshalb nicht overridden werden, sonst gibt es:
Error: There is no method in an ancestor class to be overridden: "constructor Create(Boolean);"
"Inherited" muss man trotzdem aufrufen, ist ja der Konstruktor.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: TThread constructor: warum kein "override"?

Beitrag von Nimral »

Sollte der Constructor nicht eigentlich virtual markiert sein? Ich dachte, das ist verpflichtend, wenn man das Überschreiben erlauben will --> eigentlich müssten fast alle Constructoren virtual sein, oder nicht?

Benutzeravatar
theo
Beiträge: 10497
Registriert: Mo 11. Sep 2006, 19:01

Re: TThread constructor: warum kein "override"?

Beitrag von theo »

Der ist in diesem Fall nicht überschrieben, nur darunter versteckt. :wink:

Zu dem Thema gibt es längere Diskussionen:
https://forum.lazarus.freepascal.org/in ... 366.0.html
https://www.delphipraxis.net/98594-kons ... nicht.html

Benutzeravatar
kupferstecher
Beiträge: 422
Registriert: Do 17. Nov 2016, 11:52

Re: TThread constructor: warum kein "override"?

Beitrag von kupferstecher »

Es gibt im Grunde zwei Arten vom Überschreiben. Die eine ist die einfache Version ohne virtual/override, (wie sie beim KonstruKtor standardmäßig verwendet wird). Welche Methode in der Klassenhierarchie dann gerufen wird, entscheidet sich schon zur Compilezeit und ist direkt vom Typ selber abhängig.

Bei der virtual/override-Variante dagegen kann der Variablentyp von einer beliebigen abgeleiteten Klasse sein und trotzdem wird die zur tatsächlichen Instanz passende Methode aufgerufen. Dafür ist etwas "Compiler-Magic" nötig, die man mit den Schlüsselwörtern virtual und override aktiviert.

Wenn man bspw. eine Klasseninstanz mittels Free aufräumt (z.B. MyThread.Free), ist es wichtig, dass der Destruktor mit override deklariert wurde. Free ist nämlich eine Methode von der Klasse TObject. Da alle Klassen von TObject abgeleitet sind, ist die Methode für jede Klasse verfügbar. Die Instanz wird als self-Parameter an die Methode übergeben und dort dann der Destruktor Destroy aufgerufen. In der Methode TObject.Free ist self aber vom Typ TObject, die Übergabe stellt im Grunde einen Typecast von TMyThread nach TObject dar. Es braucht nun also die "Compiler-Magic" damit auch tatsächlich wieder TMyThread.Destroy anstatt TObject.Destroy aufgerufen wird.

Vergisst man override, dann wird tatsächlich nur TObject.Destroy aufgerufen. Aber der Compiler gibt in dem Fall eine Warnung aus.


Das ist der Code von der Methode Free:

Code: Alles auswählen

      procedure TObject.Free;
        begin
           // the call via self avoids a warning
           if self<>nil then
             self.destroy;
        end;

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: TThread constructor: warum kein "override"?

Beitrag von Nimral »

Es tut mir leid, aber ich verstehe kaum ein Wort.

Ich stelle immer wieder fest, dass ich für OOP einfach zu blöd bin. Oder - was ich bequemer Weise unterstelle - OOP ist so unsagbar blöd dass es nur Genies oder noch wesentlich Blödere anwenden können, letztere lernen einfach Muster auswendig und wenden sie immer wieder an.

Zum Genie reichts wohl in der zur Verfügung stehenden Zeit nicht mehr, Ich wende ab sofort das Muster an.

Thema erledigt.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: TThread constructor: warum kein "override"?

Beitrag von Socke »

Nimral hat geschrieben:
Do 20. Jan 2022, 19:21
Sollte der Constructor nicht eigentlich virtual markiert sein? Ich dachte, das ist verpflichtend, wenn man das Überschreiben erlauben will --> eigentlich müssten fast alle Constructoren virtual sein, oder nicht?
Zumindest bei TThread kannst du interne Initialisierungen (z.B. lokale Events und Objekte erstellen usw.) in die Methode AfterConstruction auslagern. Diese wird noch vom Konstruktor-aufrufenden Thread unmittelbar nach dem Konstruktor ausgeführt.
Wichtig: In deiner AfterConstruction-Implementierung solltest du unbedingt inherited AfterConstruction aufrufen. Nur dann wird der Thread auch gestartet (falls CreateSuspended=False war).

Beispiel: https://github.com/SAmeis/pascal-future ... s.pas#L636

Code: Alles auswählen

type
  TWorkerThread = class(TThread)
  private
    AwakeEvent: PRTLEvent;
  public
    AfterConstruction; override;
  end;

procedure TWorkerThread.AfterConstruction;
begin
  // wird inherited AfterConstruction; zuerst ausgeführt, muss der Thread davon aus gehen,
  // dass die anderen Operationen in After Construction ggf. noch nicht ausgeführt sind
  AwakeEvent := RTLEventCreate;
  inherited AfterConstruction;
end;

Das finde ich sauberer als einen nicht-virtuellen Konstruktor zu überlagern.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: TThread constructor: warum kein "override"?

Beitrag von Nimral »

Socke,

danke für den Hinweis auf AfterConstruction! Ich hab mich schon mal gefragt, wofür genau es gut sein soll :-)

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: TThread constructor: warum kein "override"?

Beitrag von Socke »

Nimral hat geschrieben:
Do 20. Jan 2022, 23:21
Ich hab mich schon mal gefragt, wofür genau es gut sein soll :-)
Du kannst in AfterConstruction auch gemeinsame Aktionen ausführen, die für alle Konstruktoren gelten - sofern du denn mehrere hast. Da die meisten Klassen nur einen Konstruktur haben, erübrigt sich das meistens.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Warf
Beiträge: 1909
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: [Gelöst] TThread constructor: warum kein "override"?

Beitrag von Warf »

Es gibt zwei Arten wie eine Funktion mit gleichem namen eingeführt werden kann, shadowing und überschreiben.

Wenn du einfach eine Funktion mit selben Namen schreibst ist dies Shadowing, das heist so weil beide Funktionen immernoch existieren, du aber zugang zu der einen verdeckst mit der neuen. Der Compiler ruft immer die Funktion auf die er als erstes finden kann, also in diesem Fall:

Code: Alles auswählen

TFoo = class
  procedure FooBar;
end;

TBar = class(TFoo)
  procedure FooBar;
end;

...
var
  x: TBar;
...
  x.FooBar;
Hier schaut der Compiler nach und findet TBar.FooBar und führt diese aus. TFoo.FooBar ist somit im Schatten von TBar.FooBar und kann nicht gefunden werden, daher der begriff shadowing.
Du kannst aber immernoch TFoo.FooBar ausführen wenn du dem Compiler explizit sagst wo er suchen soll:

Code: Alles auswählen

TFoo(x).FooBar;
Die andere option ist überschreiben. Dabei existiert nur eine Referenz auf die Funktion, in der so genannten Virtual Method Table (VMT), welche überschrieben wird. Wenn dann die Funktion aufgerufen wird, wird in der VMT nachgeschaut welche Funktion eingetragen ist, und diese dann ausgeführt. Es gibt keine möglichkeit (die kein super unsauberer hack ist) die "alten" Funktionen direkt auszuführen, da sie ja überschrieben wurden.
Damit eine Funktion einen eintrag in der VMT bekommt muss diese als "virtual" markiert sein. Um eine funktion aus der VMT zu überschreiben, muss die neue Funktion als override markiert sein. Wenn die funktion nicht als override markiert ist, wird sie als nicht virtuelle funktion hinzugefügt und shadowed lediglich die virtuelle Funktion.

Code: Alles auswählen

TFoo = class
  procedure FooBar; virtual;
end;

TBar = class
  procedure FooBar; override;
end;
...
var
  x: TBar;
...
  x. FooBar; // ist das gleiche wie
  TFoo(x).FooBar;
Egal wie die funktion aufgerufen wird, es wird immer die eine aus der VMT aufgerufen.

Nur noch der vollständigkeit halber, wenn eine funktion als virual und als abstract markiert ist, wird ein eintrag in die VMT hinzugefügt, aber dort steht keine funktion drin. Somit kann angegeben sein das es eine virtuelle Funktion geben soll, die aber zu diesem Zeitpunkt noch nicht bekannt ist, und einen Fehler werfen soll falls jemand versucht sie zu verwenden.

Das ding ist Konstruktoren werden meist von der Klasse selbst aufgerufen, wenn du ClassName.Create aufrufst ist hier kein Objekt mit VMT im Spiel, weshalb virtual konstruktoren in den meisten Fällen ziemlich Sinnfrei sind. Vor allem da auch ein gewisser Overhead mit der VMT verbunden ist.
Virtuelle Konstruktoren brauchst du nur wenn du diese über den Klassentypen aufrufen willst:

Code: Alles auswählen

TFoo = class
  constructor CreateVirtual; virtual;
  constructor CreateShadowing;
end;

TBar = class(TFoo)
  constructor CreateVirtual; override;
  constructor CreateShadowing;
end;

TFooClass = class of TFoo;

var
  cls: TFooClass;
  x, y: TFoo;
begin
  cls := TBar;
  x := cls.CreateVirtual; // ruft TBar.CreateVirtual auf und erstellt ein neues Objekt vom typen TBar
  y := cls.CreateShadowing; // ruft TFoo.CreateShadowing auf (da cls vom typen class of TFoo ist) und erstellt ein neues Objekt vom typen TFoo
end;
Destruktoren hingegen sollten immer virtual sein damit man sie von egal wo in der Klassenhierachie freen kann. Um genau zu sein ruft .Free den Destructor von TObject auf, wenn dein Destruktor also nicht virtuell ist (also kein override hat), wird er bei .Free niemals aufgerufen.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: [Gelöst] TThread constructor: warum kein "override"?

Beitrag von Nimral »

Danke, Warf, ich finde, Deine Erklärung die Beste.

Ich glaube, man bekommt erst Zugang zur Frage wo der Sinn und Zweck des Ganzen ist, wenn man einen Upcast betrachtet und sich fragt, welche Methode denn nun genommen werden soll: die des Typen/Objekts das Upcasted wird, oder die des Typen zu dem gecastet wird, und wie der Compiler - wenn beide existieren - sich entscheiden soll. Da er das nicht kann, muss man ihm mit virtual und override bzw. reintroduce auf die Sprünge helfen, und wenn man das nicht tut, dankt er das mit der kontra-intuitiv fehlformulierten Meldung von der "verborgenen" Methode. So lange man ein Objekt nicht upcasted ist es egal, aber wenn man es tut, und dass man es tun kann macht ja einen großen Teil der Mächtigkeit von OOP aus, landet man bei der Methode des Vorgängers, und das ist fast immer nicht beabsichtigt.

Neuer Gedanke auf den mich Dein Posting gebracht hat: ein Upcast eines Constructors macht keinen Sinn --> also kein virtual und kein override nötig. Hätte man m.E auch beim Destructor gleich machen können, es dürften wieder "interne Gründe" oder irgendwelche Edge-cases sein, warum das nicht so gemacht wurde. Wie sollte der Destructor eines Vorgängers in der Lage sein, die Strukturen eines abgeleiteten Nachfolgers aufzuräumen? Ergo macht er nur mit virtual + override Sinn.

Ich schreib das mal so auf in der Hoffnung, dass mich jemand korrigiert, wenn diese Sicht der Dinge grob falsch wäre, immerhin hat sie für mich bisher funktioniert, und sie passt zu meinem Mindset.

Liege ich grob daneben?
Zuletzt geändert von Nimral am Fr 21. Jan 2022, 16:47, insgesamt 1-mal geändert.

PascalDragon
Beiträge: 830
Registriert: Mi 3. Jun 2020, 07:18
OS, Lazarus, FPC: L 2.0.8, FPC Trunk, OS Win/Linux
CPU-Target: Aarch64 bis Z80 ;)
Wohnort: München

Re: [Gelöst] TThread constructor: warum kein "override"?

Beitrag von PascalDragon »

Nimral hat geschrieben:
Fr 21. Jan 2022, 12:42
Neuer Gedanke auf den mich Dein Posting gebracht hat: ein Upcast eines Constructors macht keinen Sinn --> also kein virtual und kein override nötig. Hätte man m.E auch beim Destructor gleich machen können, es dürften wieder "interne Gründe" oder irgendwelche Edge-cases sein, warum das nicht so gemacht wurde. Wie sollte der Destructor eines Vorgängers in der Lage sein, die Strukturen eines abgeleiteten Nachfolgers aufzuräumen? Ergo macht er nur mit virtual + override Sinn.
Während du zwar prinzipiell recht hast, dass destructor Destroy nur mit override sinnig ist, geht es jedoch auch darum konsistente Regeln zu haben. Wenn man nun also sagt virtual und override verhalten sich so und so und wenn das nicht da ist verhält es sich anders, aber destructor Destroy verhält sich anders, dann stiftet das auch wieder Verwirrung.

Und ja, du kannst tatsächlich Desktruktoren schreiben und auch nutzen, die nicht virtuell sind (der Sinn sei mal dahin gestellt, aber es geht ;) ):

Code: Alles auswählen

program ttest;
{$mode objfpc}{$H+}

type
  TTest1 = class
    destructor Destroy; override;
  end;

  TTest2 = class(TTest1)
    destructor Destroy; override;
    destructor MyDestroy1;
    destructor MyDestroy2;
  end;

destructor TTest1.Destroy;
begin
  Writeln('TTest1.Destroy');
  inherited Destroy;
end;

destructor TTest2.Destroy;
begin
  Writeln('TTest2.Destroy');
  inherited Destroy;
end;

destructor TTest2.MyDestroy1;
begin
  Writeln('TTest1.MyDestroy1');
end;

destructor TTest2.MyDestroy2;
begin
  Writeln('TTest1.MyDestroy2');
  inherited Destroy;
end;

var
  t1: TTest1;
  t2: TTest2;
begin
  t1 := TTest2.Create;
  { will call Destroy }
  t1.Free;
  t2 := TTest2.Create;
  { will only call MyDestroy1 }
  t2.MyDestroy1;
  t2 := TTest2.Create;
  { will call MyDestroy1 and then TTest1.Destory }
  t2.MyDestroy2;
end.
Verwaltete Typen in den Elternelementen werden dabei dennoch korrekt freigegeben, aber wenn manuell Speicher alloziert wurde (zum Beispiel durch Instantiieren einer weiteren Klasse oder durch New), dann wird dieser bei MyDestroy1 eben nicht freigegeben (was dann letztlich ein Speicherleck ist).

Übrigens noch ein Hinweis zu inherited: Das hat nichts mit virtual oder nicht zu tun, das kannst du immer aufrufen, um eine Methode der Elternklasse aufzurufen. Dabei kannst du das explizit machen (so wie ich das im Code mit inherited Destroy gemacht habe oder du nutzt die Kurzform von nur inherited, wobei jedoch die Elternmethode die gleichen Parameter haben muss (und es werden auch einfach die (eventuell geänderten) Parameter weitergereicht).
FPC Compiler Entwickler

Antworten