Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-devel / 2020-06

Allow object type property getter to expose a field as one of its base/parent types (patch included)

4 messages · 2 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Kostas Michalopoulos

Hello,

I'd like to add a small feature for property getters to allow themselves be exposed as a base type of the real field. Example:

===
program ExposeDerivedAsBase;
{$mode objfpc}
type
  TBase = class
  end;
  TDerived = class(TBase)
  end;
  TSomething = class
  private
    FProp: TDerived;
  public
    property Prop: TBase read FProp;
  end;
begin
end.
===

The reasoning for this patch is to expose fields directly, without any additional overheads while being able to hide the real type that the object uses internally.

One use on a project that i'm currently working on is collections that are implemented as two classes, one base class that provides a read-only view of the items in the collection and a derived class that provides read-write access (this is similar to how C# has List<T> implement IReadOnlyList<T> but using a class instead). This approach is actually implemented in two collections, one that uses object types so that the collection data can be embedded inside other types (as opposed to creating separate objects on the heap) and one that uses class types that wraps around the object type (this is mainly to be able to expose the collection to RTTI for a custom serialization system i'm working on that relies on properties and custom attributes for its functionality).

You can find a Mantis entry with the patch here:
https://bugs.freepascal.org/view.php?id=37175

This basically adds an additional check in
add_getter_or_setter_for_sym when the call to compare_defs doesn't return an adequate value to see if the relevant defs are compatible objects, which should not introduce any additional overhead in existing code (initially i considered modifying compare_defs_ext but i noticed that it is used in several places so i found this special case safer).

Kostas Michalopoulos

Reply by email

Sven Barth
to Kostas Michalopoulos
Kostas Michalopoulos wrote 26 lines
Hello, I'd like to add a small feature for property getters to allow themselves be exposed as a base type of the real field. Example: === program ExposeDerivedAsBase; {$mode objfpc} type TBase = class end; TDerived = class(TBase) end; TSomething = class private FProp: TDerived; public property Prop: TBase read FProp; end; begin end. === The reasoning for this patch is to expose fields directly, without any additional overheads while being able to hide the real type that the object uses internally.

I'm against it. It would essentially mean that you can't use the same field for the setter or that you'd need to go through a method. Exchanging one restriction (can't use a base class for the property) for another (can't use the same field for the setter) isn't worth it especially as you can avoid performance issues by using a getter method that is declared inline (the only case where this won't help is if you access it using RTTI, but one call more or less doesn't really change much anyway).

Regards,
Sven

Reply by email

Kostas Michalopoulos
to Sven Barth
PM Sven Barth wrote 8 lines
I'm against it. It would essentially mean that you can't use the same field for the setter or that you'd need to go through a method. Exchanging one restriction (can't use a base class for the property) for another (can't use the same field for the setter) isn't worth it especially as you can avoid performance issues by using a getter method that is declared inline (the only case where this won't help is if you access it using RTTI, but one call more or less doesn't really change much anyway).

These two restrictions are not equal, using a setter method is much more common than using a getter that accesses the field directly. Also there is no exchange since you cannot use a property with a base type as a setter for the base field. None of this functionality already exists, this patch does not introduce any new restriction, it only removes an existing one (which is even inconsistent since in other places you can use values as their base type when the base type is needed).

Performance-wise inlining doesn't always work (more often than not the compiler fails to inline methods, especially when nesting inlines) and so it cannot be relied upon. In addition, using a getter method does change the RTTI data which is a significant difference.

To give you an idea on how i'm using this, i have a generic object type (not class) that provides a read only view of a collection and a mutable view that extends it, like this:

generic TDynArrayView<T> = object ...
generic TDynArray<T> = object(specialize TDynArrayView<T>) ...

This allows me to use a property like

property Items: specialize TDynArrayView<T> read FItems; // FItems is TDynArray

that always provides direct access to the items but without the API allowing any modifications that ensures all modifications to the collection itself will be done through the methods of the class that provides the property but all accesses will have minimum overhead.

The RTTI data is also important because i have a custom serialization system that tries to automate as much as possible with minimal hand-holding (i also use custom attributes for this). All objects are assumed to descent from a TRootObject object that provides functions like SerializeToStream/DeserializeFromStream and this does deep de/serialization (ie. serializes all objects, not just just the root one).

This takes advantage of getter-only field-access properties to assume that they are meant to also be serialized and owned by the object itself (which is a safe assumption for most objects, custom attributes can be used to change the behavior) so when deserializing, it writes to those getter-only field-access properties directly. The implication of this is that the address of the field must be available and thus an getter function wouldn't work (before doing the patch i did try to take the offset of the field variable and store it in a custom attribute as a workaround, but it didn't work since parameters to custom attributes do not seem to be able to do that - and besides i'm not a fan of this since it adds extra noise to the declaration and custom attributes seem to be unable to have any statically allocated data and you need to allocate new instances just to access their data
- though that is another issue that i have with custom attributes in general).

What the above means is that i can have two objects like

TSomething = class(TRootObject) ...

TAnother = class(TRootObject)
private
  FSomething: TSomething;
published
  property Something: FSomething read FSomething;
end;

and then when i do Another.SerializeToStream(SomeStream) and Another.DeserializeFromStream(SomeStream) i can have the TSomething instance automatically be stored and recreated without any additional hand-holding boilerplate code (you need to provide a default Create constructor, but this is basically the same requirement like TPersistent). I also have a THidden custom attribute which is used to mark published properties that are not to be exposed to the UI.

Also note that i use both of the above cases with classes like

generic TItemCollectionView<T> = class(TRootObject) ... generic TItemCollection<T> = class(specialize TItemCollectionView<T>) ...

Which allows me to automatically de/serialize collections owned by objects without any additional custom code (these two classes actually wrap the TDynArray generic objects i mentioned above).

So basically as you see not only inline functions aren't equivalent (they change the RTTI data, they can potentially be slower - especially when considering value types like objects) but i'm actually having two real use cases for this functionality.

I cannot think of a use case for the restriction you mention about allowing to write to the setter (and again, that wouldn't make sense logically either - you can "get" a base type value from a derived type value, but nowhere in the language you can "put" a base type value to a derived type value since that base type might be some other derived type that doesn't match).

Reply by email

Kostas Michalopoulos
to Kostas Michalopoulos

Any further thoughts on this? I am already using it in a new project for quite some time now and it has proven to be very useful in several cases by exposing read only views of the same container that is used internally by classes without additional boilerplate and with fast access and rtti availability.

TBH I've found it so useful that even if it isn't included I'll maintain the patch myself (it is tiny and the relevant code doesn't seem to be modified frequently anyway :-P), but I think it is a very useful feature for everyone because of what it enables you to do (see example in my last message). Not having it feels like going back to pre-generics FP or pre-dynamic arrays Delphi; sure, you do not need the functionality, but it is better when the compiler enables you to avoid busywork and let you focus on more important parts :-P

Kostas

Kostas Michalopoulos wrote 106 lines
On Sat, Jun 13, 2020 at 12:07 PM Sven Barth via fpc-devel wrote: > I'm against it. It would essentially mean that you can't use the same > field for the setter or that you'd need to go through a method. > Exchanging one restriction (can't use a base class for the property) for > another (can't use the same field for the setter) isn't worth it > especially as you can avoid performance issues by using a getter method > that is declared inline (the only case where this won't help is if you > access it using RTTI, but one call more or less doesn't really change > much anyway). These two restrictions are not equal, using a setter method is much more common than using a getter that accesses the field directly. Also there is no exchange since you cannot use a property with a base type as a setter for the base field. None of this functionality already exists, this patch does not introduce any new restriction, it only removes an existing one (which is even inconsistent since in other places you can use values as their base type when the base type is needed). Performance-wise inlining doesn't always work (more often than not the compiler fails to inline methods, especially when nesting inlines) and so it cannot be relied upon. In addition, using a getter method does change the RTTI data which is a significant difference. To give you an idea on how i'm using this, i have a generic object type (not class) that provides a read only view of a collection and a mutable view that extends it, like this: generic TDynArrayView<T> = object ... generic TDynArray<T> = object(specialize TDynArrayView<T>) ... This allows me to use a property like property Items: specialize TDynArrayView<T> read FItems; // FItems is TDynArray that always provides direct access to the items but without the API allowing any modifications that ensures all modifications to the collection itself will be done through the methods of the class that provides the property but all accesses will have minimum overhead. The RTTI data is also important because i have a custom serialization system that tries to automate as much as possible with minimal hand-holding (i also use custom attributes for this). All objects are assumed to descent from a TRootObject object that provides functions like SerializeToStream/DeserializeFromStream and this does deep de/serialization (ie. serializes all objects, not just just the root one). This takes advantage of getter-only field-access properties to assume that they are meant to also be serialized and owned by the object itself (which is a safe assumption for most objects, custom attributes can be used to change the behavior) so when deserializing, it writes to those getter-only field-access properties directly. The implication of this is that the address of the field must be available and thus an getter function wouldn't work (before doing the patch i did try to take the offset of the field variable and store it in a custom attribute as a workaround, but it didn't work since parameters to custom attributes do not seem to be able to do that - and besides i'm not a fan of this since it adds extra noise to the declaration and custom attributes seem to be unable to have any statically allocated data and you need to allocate new instances just to access their data - though that is another issue that i have with custom attributes in general). What the above means is that i can have two objects like TSomething = class(TRootObject) ... TAnother = class(TRootObject) private FSomething: TSomething; published property Something: FSomething read FSomething; end; and then when i do Another.SerializeToStream(SomeStream) and Another.DeserializeFromStream(SomeStream) i can have the TSomething instance automatically be stored and recreated without any additional hand-holding boilerplate code (you need to provide a default Create constructor, but this is basically the same requirement like TPersistent). I also have a THidden custom attribute which is used to mark published properties that are not to be exposed to the UI. Also note that i use both of the above cases with classes like generic TItemCollectionView<T> = class(TRootObject) ... generic TItemCollection<T> = class(specialize TItemCollectionView<T>) ... Which allows me to automatically de/serialize collections owned by objects without any additional custom code (these two classes actually wrap the TDynArray generic objects i mentioned above). So basically as you see not only inline functions aren't equivalent (they change the RTTI data, they can potentially be slower - especially when considering value types like objects) but i'm actually having two real use cases for this functionality. I cannot think of a use case for the restriction you mention about allowing to write to the setter (and again, that wouldn't make sense logically either - you can "get" a base type value from a derived type value, but nowhere in the language you can "put" a base type value to a derived type value since that base type might be some other derived type that doesn't match).

Reply by email