Jump to content

Recommended Posts

Tried to use generics to parametrize a class with some code.

type
  TTest1 = class
    procedure Test();
  end;

  TTest2 = class(TTest1)
    procedure Test();
  end;
  
  TTest<T: TTest1, constructor> = class(TObject)
    FTest: T;
    procedure TestIt();
  end;

When calling FTest.Test() I expect a method of actual type parameter to be called.

procedure TTest<T>.TestIt();
begin
  FTest := T.Create();
  FTest.Test();
  readln;
end;

But actually a method of type constraint (!) is being called.

I.e. static type of FTest not depend on actual type parameter. It's always TTest1!

Is there a workaround for this? How do you parametrize generic classes with code?

Comparators, hash calculators and other useful things which give generics a purpose.

It's an essential part of generics which simply not work! How this bug can not even be reported till now?

The code works as expected with Free Pascal Compiler.

https://quality.embarcadero.com/browse/RSP-27840

Edited by Georgge Bakh

Share this post


Link to post

constructor constraint isn't going to be much use here, you can get rid of it. What you need is a virtual constructor on the base class, and virtual methods. And then obviously to override those methods on subclasses. 

Share this post


Link to post

If I'd wanted virtual methods I wouldn't use generics. Just field of some base class is enough.

But it's not a compile time parametrization. It's runtime technique.

5 hours ago, Eugine Savin said:

This shouldn't work. Make method Test virtual and override it in Test2

 

Why? My logic is: TTest<T> has field FTest of type T. Therefore specialized TTest<TTest2> will have field FTest of type TTest2. How the field can be of type TTest1? TTest1 is just a type parameter constraint after all. It's needed only to allow the use of Test() method in code.

Share this post


Link to post
7 minutes ago, Georgge Bakh said:

Why?

Because that's the only way to make this work.

 

Your expectations seem unrealistic.  As I see it you face two choices:

1. Code it the way I said, and thus have your code work the way you want.

2. Code it your way, and have your code not work the way you want.

 

I don't understand why you want to take option 2.

Share this post


Link to post

David, if I got you right, your advice is to use the technique with virtual methods because it works. It's a good advice thank you.

But I wanted to use generics as it's a powerful technique which I successfully use in other languages and it seems it should work for my case. And a broad range of other cases which can be identified as parametrization by code.

If it can't be done in Delphi it's sad but I'd want to know why. Is it a bug?

Let's get on with it:

I have TTest<TTest2> specialization of the above generic class TTest. May I expect that field FTest will have static type TTest2? If no why?

Share this post


Link to post
21 minutes ago, Kryvich said:

How would you write it in C#? I tried to write it like below, but it doesn't even compile:

You mix static and non static?

Remove 'static' and new TTest<TTest2>().TestIt(); prints "TTest1.Test"

 

https://dotnetfiddle.net/S3DAGW

Edited by Attila Kovacs

Share this post


Link to post

@Attila Kovacs Yes, I've found my errors. (Never programmed generics in C# before).

Updated code in C#: https://dotnetfiddle.net/010hlp

I should say it has absolutely the same behavior as Delphi's one. Perhaps Free Pascal developers understand Generics a little differently.

Edited by Kryvich

Share this post


Link to post
1 hour ago, Kryvich said:

How would you write it in C#? I tried to write it like below, but it doesn't even compile:

https://dotnetfiddle.net/OACCH6

How about you write the code in a proper way:

 

using System;

public class TTest1 {
	public void Test() {
		Console.WriteLine("TTest1.Test");
	}
}

public class TTest2: TTest1 {
	public void Test() {
		Console.WriteLine("TTest2.Test");
	}
}

public class TTest<T> where T: TTest1, new() {
    public static void TestIt() {
		var FTest = new T();
		FTest.Test();
	}
}

public class Program
{
	public static void Main()
	{
		TTest<TTest2>.TestIt();
	}
}

The reason FPC does it different is because its generics are behaving more like C++ templates. However for generics there is no kinda "duck typing" or prototyping. You tell the generic class "look there is the type parameter T, which is guaranteed to be a TTest1 and it has a constructor". So when the compiler generates the AST for the generic type it just takes from these informations and so it decides to emit a static call to TTest1.Test because that is not a virtual method.

This is the behavior in all languages with generics that I know of (such as C# or Java) - languages that use a templating approach such as C++ and possibly FPC does it similar decide what to call when the type parameter is being specified.

 

Generics in this case behave the same way as if you would write code like this:

 

procedure TestIt(const t: TTest1);
begin
  t.Test;
end;

here it will always call TTest1.Test and not TTest2.Test even it t is a TTest2 - simply because there is no polymorphism happening due to the lack of a virtual method call.

 

Personally I would rather call this a bug in FPC - but again it depends on the actual language specification/implementation. Both have their pros and cons. I cannot find the official specification for generics in FPC about this specific point to verify.

Edited by Stefan Glienke
  • Like 2

Share this post


Link to post
5 minutes ago, Kryvich said:

Never program generics in C# before

I have never seen c# before. And we were living in a small shoebox in the middle of the road.

Now, hijacking the topic for a small moment, how do you make this c# snipplet to print "TTest2.Test"?

Share this post


Link to post
4 minutes ago, Attila Kovacs said:

I have never seen c# before. And we were living in a small shoebox in the middle of the road.

Now, hijacking the topic for a small moment, how do you make this c# snipplet to print "TTest2.Test"?

You make the Test method virtual just like was suggested in the 2nd post for the similar Delphi code.

You can then even make the override in TTest2 final or make the class sealed and get a non virtual call generated by the compiler!

Edited by Stefan Glienke

Share this post


Link to post

@Stefan Glienke I've tried, without success. "public virtual void Test()" in TTest1 and "public new void Test() {" in TTest2.

That's why I'm asking, but really don't want to convert this conversation into a c# for dummies (me).

Share this post


Link to post
2 minutes ago, Attila Kovacs said:

@Stefan Glienke I've tried, without success. "public virtual void Test()" in TTest1 and "public new void Test() {" in TTest2.

That's why I'm asking, but really don't want to convert this conversation into a c# for dummies (me).

public new is not overriding - that's similar to reintroduce - if you want to override, you write *drumroll* override

 

I would even guess the CLR to be clever enough to devirtualize it entirely

Edited by Stefan Glienke
  • Haha 1

Share this post


Link to post
2 minutes ago, Attila Kovacs said:

@Stefan Glienke Thx a lot! I just followed the compiler hints 😉

Then you probably should read them again - they say:

warning CS0114: 'TTest2.Test()' hides inherited member 'TTest1.Test()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

Share this post


Link to post
1 hour ago, Georgge Bakh said:

David, if I got you right, your advice is to use the technique with virtual methods because it works. It's a good advice thank you.

But I wanted to use generics as it's a powerful technique which I successfully use in other languages and it seems it should work for my case. And a broad range of other cases which can be identified as parametrization by code.

If it can't be done in Delphi it's sad but I'd want to know why. Is it a bug?

Let's get on with it:

I have TTest<TTest2> specialization of the above generic class TTest. May I expect that field FTest will have static type TTest2? If no why?

It's not a choice between either generics, or polymorphism. You can use both.

Share this post


Link to post

OK, C# is an authority here and my approach is totally wrong.

But how it can be achieved in compile time?

The task is simple: I have a class, let's call it TDataHandler which use another class as data provider - TDataProvider.

TDataProvider can be a simple wrapper over a memory buffer or more complicated stream-based implementation or something else.

In TDataHandler I call TDataProvider to get some data and several other data-related things.

I could declare methods of TDataProvider as virtual and override them in actual implementation but I don't want to have virtual method calls (non-virtual is acceptable) instead of simply referencing a pointer (as generated by FPC).

How it can be done?

Please note that in Java (and probably C#) virtual calls is not a problem as JIT compiler will inline even virtual calls if needed and the resulting machine code will be similar to one generated by FPC.

Share this post


Link to post

As I wrote before a final method or sealed class will produce a non virtual call in your approach - try again the code from your original post with virtual in TTest1 and override final in TTest2.

Also let me add that the fear of virtual method calls is way too high and in most cases it turns out they don't affect the overall performance at all (especially not with some code that the Delphi compiler produces that trashes your performance in other ways)

 

If you call a virtual method in a tight loop and want to avoid the "virtual method address lookup" (again, measure if it really affects anything) that happens every time then you can store it as a method pointer before and then call that one in your loop.

Edited by Stefan Glienke

Share this post


Link to post

@Georgge Bakh FPC approach isn't wrong, but different.

Other Points: 

Quote

1. The compiler parses a generic, but instead of generating code it stores all tokens in a token buffer inside the PPU file.
2. The compiler parses a specialization; for this it loads the token buffer from the PPU file and parses that again. It replaces the generic parameters (in most examples "T") by the particular given type (e.g. LongInt, TObject).
The code basically appears as if the same class had been written as the generic but with T replaced by the given type.

 

Let's experiment: will do the specialization manually as the Wiki suggests.

program ClassConstTest2;
{$APPTYPE CONSOLE}
{$R *.res}

type
  TTest1 = class
    procedure Test();
  end;

  TTest2 = class(TTest1)
    procedure Test();
  end;

  TTestTest2 = class
    FTest: TTest2;
    procedure TestIt();
  end;

procedure TTestTest2.TestIt();
begin
  FTest := TTest2.Create();
  FTest.Test();
  readln;
end;

procedure TTest1.Test;
begin
  Writeln('TTest1.Test');
end;

procedure TTest2.Test;
begin
  Writeln('TTest2.Test');
end;

var
  Test2: TTestTest2;

begin
  Test2 := TTestTest2.Create;
  Test2.TestIt;
end.

This code outputs TTest2.Test

Edited by Kryvich

Share this post


Link to post

@Kryvich

Just curious - what else could output this code?

 

I'm aware of FPC approach to generics and it's much better IMHO. Although not complete too.

 

As of virtual calls - when writing a low-level library code there is not known where exactly it will be used. So trying to avoid virtual calls it normal especially when those are absolutely unnecessary.

 

Thanks for help. Later will check and bench with sealed/final.

And will decide on a choice to use ugly IFDEFS or drop Delphi support. :classic_sad:

 

Share this post


Link to post

I've checked. As Stefan said if TTest2 is sealed or its method Test() is final the call is non-virtual.

If type parameter of TTest is constrained by an interface the correct method is called but despite non-virtual declaration of Test() method the call looks like virtual (FPC inlined the call).

GetTypeName(TypeInfo(T)) is 'TTest2' in both cases.

Overall the current behaviour of generics in Delphi seems to be too restrictive without any gain.

May be in C# it's acceptable because it does have many other capabilities and aggressively optimizing JIT compiler. But for Delphi it's not suitable.

Edited by Georgge Bakh

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×