Goodbye, TObject

I’m not using TObject in my Delphi code anymore. Sure I use the core classes like TStream derived from TObject but I don’t derive my own classes from TObject or descendant classes. I am using a different OOP paradigm instead.
It all started when I was looking for a simple-to-use and efficient BigInteger type in Delphi and the solution seems absolutely evident now when I am looking back at it:

type
  IBigNumber = interface
/*
  interface methods
*/
  end;

type
  BigInteger = record
  private
    FNumber: IBigNumber;
  public
/*
  public methods and overloaded operators
*/
  end;

The BigInteger type is an interface reference wrapped in the advanced record; SizeOf(BigInteger) = SizeOf(Pointer).

Let us generalize the above idea. Instead of

type
  TMyObject = class
/*
..
*/
  end;

we use the alternative:

type
  IMyObject = interface
/*
..
*/
  end;

  TMyObject = record
  private
    FMyObject: IMyObject;
  public
/*
..
*/
  end;

You can see that the alternative requires more code typing. What we obtain for more work?

  • IMyObject can be implemented in DLL and used with different programming languages;
  • We obtain automatic memory management based on reference counting;
  • Automatic memory management makes possible fluent coding;
  • We can overload operators in TMyObject;
  • We get rid of all TObject overhead.

You may ask what about inheritance and polymorphism (late binding)? These are moved from classes (TMyObject) to interfaces (IMyObject), and the move makes coding more elastic. We now have separate inheritance of interfaces and implementations, and interfaces are always polymorphic (late bound).
You can notice that interfaces in Delphi are implemented using classes derived from TInterfacedObject but as I posted before this is not necessary – interfaces can be implemented in a more compact and efficient way.

And this closes the ring; TObject is not needed.

Advertisements

7 thoughts on “Goodbye, TObject

  1. Interesting, though that still leaves open the issue of circular references, which any non-trivial collection will have.

    Also, fluent coding in Delphi isn’t made difficult by the memory management, but by the lack of debugger support: can’t breakpoint in a fluent expression, can’t evaluate intermediate values, problematic call stack traces…

  2. Good luck with that approach.
    I like it for things like your BigInteger (where it kinda fakes a simple value type) – I used it for things like Event, Lazy, Nullable and such.

    But replacing objects I don’t think that will be the best way.
    – instead of direct field access in simple classes you now jump through at least 2 method calls
    – no inheritance on the record level (have fun implementing numerous operator overloads to fake that)
    – no default constructor (sure, you can write a class function) on records and the extra overhead to make sure that the interface reference is getting created.

  3. I agree with Stefan, I don’t see how this is better then objects, it adds to many restrictions and also adds depth to access as you wrap interface inside a record. Also you say you get rid of TObject overhead, but on the other hand you get interface overhead which is actually worse, so your code will be slower and less efficient. I like semi-fluent interfaces like the ones I used in my SimpleStorage for XML but that can be done when needed and is not a silver bullet.

    At least you could use my approach on TAnyValue where I hooked the record management functions to allow for constructor and destructor. If you make a general record for what you did you can then have automatic constructor destructor calls. But I have no solution for x64 yet as the Detours I use can’t work with x64.

    My humble opinion is that you are over-enginering things here or at least complication for very little gain. Your approach can be very valuable for certaion types of problems but it is not a silver bullet. By the way the next gen compiler already does all that.

    But thanks for the post. I am only trying to be positively critical 🙂

  4. You’re using one clumsy technique (implementing interfaces with records) + another clumsy technique (described here), which together require typing the same thing three times. To do what? Get rid of TObject? Why? It’s not ideal, but you know, after seeing this alternative I guess I’m gonna marry it.

    Seriously though, I’ve just writen a simple benchmark. It calls given function 10 million times, then prints results:
    Static records… 327 ticks, 30581,0398 tests/tick
    Dynamic records…125 ticks, 80000 tests/tick
    Dynamic records + init… 359 ticks, 27855,1532 tests/tick
    TObject… 546 ticks, 18315,0183 tests/tick
    IInterface… 905 ticks, 11049,7238 tests/tick

    Static records and dynamic records+init both did Initialize() because the record had string field in it. That is the biggest offender. Dynamic record, even though it uses GetMem, managed to outperform stack-allocated record by not calling it. The difference between full-blown TObject and “cheap” record with string field is comparable to that, and less than the difference between that and TObject+IInterface, in other words, using TObject is cheaper than just *adding* the IInterface overhead (not implementing IInterface from scratch, just the overhead).

    Why is the initialize so slow? Because it’s common for all records. It uses some kind of RTTI to determine how to initialize the record. This is slow. How to make it faster? Generate custom initializers for every record type. It’s not that hard I suppose. How to make it even faster? Give us custom lightweight c++-style types where we can write our own initializers.

    • I think Delphi language needs default constructors and destructors for record types; these should never be called explicitely by a programmer but invoked by the compiler like in C++, to implement custom initialization and finalization.

      • It basically needs C++ style objects. One thing C++ does right is objects. Cheap, powerful, can be made into almost anything. They have default constructors/destructors which are pre-generated and efficient but can be overriden. They also unify objects and records (and add inheritance to the latter).

      • @Himselfv: You’ve gotta be kidding. Objects are one of the things C++ does entirely, absolutely wrong. There’s a good reason why no other language (except D, which is based on C++) borrows anything even resembling C++’s object model: because C++ got it completely wrong.

        Objects-as-value-types screws up Liskov substitution, which is the core of OOP. Then you end up with horrendous messes when it comes to inheritance, not to mention having to write copy constructors for everything. (And = operators; yay for mandatory duplicate functionality!)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s