PDA

View Full Version : Constructor casting



Brainer
28-09-2008, 08:34 AM
Hello. :)

Sorry for the misleading topic title, but I don't really know how to describe it. :? First of all, consider this code:

program Project1;

{$APPTYPE CONSOLE}

uses
FastMM4,
Classes,
SysUtils;

type
{ .: TClass1 :. }
TClass1 = class(TObject)
private
{ Private declarations }
FVal: Single;
FList: TList;
public
{ Public declarations }
constructor Create();
destructor Destroy(); override;

function AddIt(): TClass1;
property X: Single read FVal write FVal;
end;

{ .: TClass2 :. }
TClass2 = class(TClass1)
public
{ Public declarations }
constructor Create(const X: String);

procedure WriteVal();
end;

{ TClass1 }

function TClass1.AddIt: TClass1;
begin
Result := TClass1.Create();
FList.Add(Result)
end;

constructor TClass1.Create;
begin
inherited Create();

FList := TList.Create();
end;

destructor TClass1.Destroy;
var
I: Integer;
begin
for I := FList.Count -1 downto 0 do
TClass1(FList.Items[I]).Free();
FList.Clear();
FList.Free();

inherited Destroy();
end;

{ TClass2 }

constructor TClass2.Create(const X: String);
begin
inherited Create();

writeln('From class2: ', X);
end;

procedure TClass2.WriteVal;
begin
writeln(X:0:2);
end;

var
C: TClass1;
begin
C := TClass1.Create();
try
C.X := 3.14;
with TClass2(C.AddIt()) do
begin
X := 5.25;
WriteVal();
end;
finally
FreeAndNil(C);
end;

readln;
end.


Now I'd like to know how to call the TClass2 constructor using that casting:

with TClass2(C.AddIt()) do


Is it possible? :?

Thank you in advance and I hope it's clear for all.

chronozphere
28-09-2008, 09:09 AM
As far as i can tell it's possible. :)

Some time ago i've seen this:



with TMyObjectThing.Create(MyParam) do
begin
//do things with TmyObjectThing

end;


Did you test the code?

Brainer
28-09-2008, 09:28 AM
This one works,

with TClass2(C.AddIt()).Create('dox') do

But only partially, as FastMM informs about memory leaks from TClass1. :?

User137
28-09-2008, 10:08 AM
Indeed. C.AddIt() will not be freed in code and results memory leak that i don't know how Delphi handles.

Edit: with TClass2(C.AddIt()) do
begin
X := 5.25;
WriteVal();
Free; // Add this and no memory leak
end;

However, because Class2 may have properties that Class1 don't, there may be problems with compatibility. C.AddIt() will only reserve memory for variables in TClass1, anything that goes to TClass2 goes out of its memory.. sort of access violation at least in theory :P

michalis
28-09-2008, 01:50 PM
Hmm, what are you trying to achieve?

Testing in FPC in Delphi mode (just removed FastMM and compiled with "fpc -Mdelphi -gh a.pas", -gh adds memory leak checking) the code works. It should also work with Delphi, at least non-.net versions, as basic class implementation is the same there (as in all OOP languages). However, it works only by accident, as far as I see.

Reason: you create in TClass1.AddIt an instance of class TClass1. But later you want to cast it to class TClass2, which is simpy an invalid cast... The fact that it works, without introducing any memory leaks or access violations, is only an coincidence, a result of how compilers implement classes and a result of the fact that TClass2 does not introduce any new variables and does not introduce any new virtual methods.

But you're doing something inherently wrong, without any guarantee that it will always work... Changing unchecked cast "TClass2(C.AddIt())" to a checked cast "C.AddIt() as TClass2" reports "Invalid type cast", showing that the cast is wrong.

If you really need to cast C.AddIt result to a TClass2, you should just create TClass2 inside TClass1.AddIt. Like


........ { replacing with dots unchanged code }

type
{ Forward reference only, TClass2 wil be declared later. }
TClass2 = class;

TClass1 = class(TObject)
.......
function AddIt(): TClass2;
.......
end;

TClass2 = class(TClass1)
......

.........

function TClass1.AddIt: TClass2;
begin
Result := TClass2.Create('blah');
FList.Add(Result)
end;

............

var
C: TClass1;
begin
C := TClass1.Create();
try
C.X := 3.14;
with C.AddIt() do
begin
X := 5.25;
WriteVal();
end;
finally
FreeAndNil(C);
end;

readln;
end.


Now the cast is even not needed, and all is correct.

grudzio
28-09-2008, 05:44 PM
If You want something more flexible, You can use class objects to create any object derived from TClass1. But it requires that all constructors have same list of parameters. Here is a sample code:


type
TClass1 = class;
TClass1Object = class of TClass1; //a class object (metaclass)

TClass1 = class(TObject)
...
constructor Create;
function AddIt(classType : TClass1Object): TClass1;
...
end;

TClass2 = class(TClass1)
...
constructor Create; //must have same signature as TClass1.Create
end;

...

function TClass1.AddIt(classType : TClass1Object): TClass1;
begin
Result := classType.Create;
FList.Add(Result)
end;

...

var
C: TClass1;
begin
C := TClass1.Create();
try
C.X := 3.14;
with C.AddIt(TClass2) as TClass2 do
begin
X := 5.25;
WriteVal();
end;
finally
FreeAndNil(C);
end;

readln;
end.

Brainer
28-09-2008, 05:59 PM
I'm aiming for something very similar like grudzio suggested, but I'd like to have different parameters in constructors. Is it possible at all? :roll:

michalis
28-09-2008, 06:34 PM
If You want something more flexible, You can use class objects to create any object derived from TClass1. But it requires that all constructors have same list of parameters.


Remember that constructor must be virtual in TClass1, and overridden in TClass2 and other descendants then. Otherwise classType.Create always calls TClass1 constructor. As a side effect, compiler will also check then that Create "signatures" are matching in both classes (you can override only existing proc..).

grudzio
28-09-2008, 06:38 PM
First thing that comes to my mind is to leave constructors as they are and add Init procedure with dofferent parameters. So the code would look like this.

type
TClass1 = class;
TClass1Object = class of TClass1; //a class object (metaclass)

TClass1 = class(TObject)
...
constructor Create; virtual;
procedure Init; virtual;
function AddIt(classType : TClass1Object): TClass1;
...
end;

TClass2 = class(TClass1)
...
constructor Create; override; //must have same signature as TClass1.Create
procedure Init(const s : string); reintroduce;
end;

...

function TClass1.AddIt(classType : TClass1Object): TClass1;
begin
Result := classType.Create;
FList.Add(Result)
end;

procedure TClass1.Init;
begin
writeln('Simple init');
end;

...

procedure TClass2.Init(const s : string);
begin
writeln('Init '+s);
end;

var
C,C1: TClass1;
begin
C := TClass1.Create();
try
C.X := 3.14;
C1 := C.AddIt(TClass2);
if C1 is TClass2 then
(C1 as TClass2).Init('C1')
else
C1.Init;
C1.X := 5.25;
WriteVal();
finally
FreeAndNil(C);
end;

readln;
end.

But there is a problem with the if statement. If You add another class (TClass3) with another version of Init then You need to update the if statement. That is why constructors (and init functions) should have same signature. The only way to fix it that I can think of, is to use some flexible data format which can hold different data types. This could be XML, JSON or your own custom data format. So the code would be like this.

type
TClass1 = class;
TClass1Object = class of TClass1; //a class object (metaclass)

TClass1 = class(TObject)
...
constructor Create; virtual;
procedure Init(data : TXMLNode); virtual;
function AddIt(classType : TClass1Object): TClass1;
...
end;

TClass2 = class(TClass1)
...
constructor Create; override;//must have same signature as TClass1.Create
procedure Init(data : TXMLNode); override;
end;

...

function TClass1.AddIt(classType : TClass1Object): TClass1;
begin
Result := classType.Create;
FList.Add(Result)
end;

procedure TClass1.Init(data : TXMLNode);
begin
//do something with data
end;
...

procedure TClass2.Init(data : TXMLNode);
begin
//do something else with data
end;

var
C: TClass1;
data : TXMLNode;
begin
loadData(data); //get data
C := TClass1.Create();
C.Init(nil);
try
C.X := 3.14;
with C.AddIt(TClass2) do //type casting is not needed anymore
begin
X := 5.25;
Init(data);
WriteVal();
end;
finally
FreeAndNil(C);
end;

readln;
end.

Of cousrse, the Init procedure can be integrated into the AddIt function.
[EDIT] I've corrected the code as pointed by Michalis.

michalis
28-09-2008, 06:39 PM
I'd like to have different parameters in constructors. Is it possible at all? :roll:

Overridden constructors must have the same parameters, otherwise compiler doesn't know how to call them. (And you don't know that either --- you do you want to call classType.Create, if you don't know whether it should be classType.Create(123) or classType.Create('aaa') ?).

Various solutions are possible here. E.g. you can make a couple of overloaded virtual constructors. Or you can make one constructor that takes something like "a: array of const", this will allow you to pass variable-length array with any types, like for Format. Or you can use variants (many Pascal programmers dislike them, but sometimes...).

Brainer
28-09-2008, 07:30 PM
The main purpose of what I'm trying to achieve is a hierarchy for my engine. I want to make a flexible class that will let me create children within it, so I don't need to create them in my main app. For example:

// ** I DO NOT WANT IT WORK LIKE THIS **
var
X: TClass1;
Y: TClass2;
begin
X := TClass1.Create('objectx');
Y := TClass2.Create('objecty', 3.14);
try
X.Add(Y);
finally
X.Free(); // Y is freed here
end;
end;

// ** INSTEAD, I WANT IT LOOK LIKE THIS **
var
X: TClass1;
begin
X := TClass1.Create('objectx');
try
with TClass2(X.AddNew(TClass2)) do
// do something
finally
X.Free();
end;
end;

I could easily achieve this using metaclasses, but the problem is that I wanna pass an extra parameter to the TClass2 constructor.

Assuming for above posts, it is not possible, am I right?

michalis
28-09-2008, 09:02 PM
Assuming for above posts, it is not possible, am I right?

It is possible, it seems both Grudzio's (virtual simple constructor + Init methods) and my (like virtual constructor taking "array of const") ideas could be useful for you :)

Brainer
29-09-2008, 12:01 PM
Ok, I'll give them both a shot, thanks a lot, men! :D