Why can't Delphi records have inheritance?
Asked Answered
P

8

19

Something I've wondered for a long time: why aren't Delphi records able to have inheritance (and thus all other important OOP features)?

This would essentially make records the stack-allocated version of classes, just like C++ classes, and would render "objects" (note: not instances) obsolete. I don't see anything problematic with it. This would also be a good opportunity to implement forward declarations for records (which I'm still baffled as to why it's still missing).

Do you see any problems with this?

Promiscuous answered 19/1, 2010 at 17:39 Comment(5)
When you say protoyping, I think the term you really mean is forward declarations.Psychrometer
Yes, sorry, momentary memory lapse. :P It's basically the same thing, just spelled differently between the Delphi and C++ world. Though I prefer the Delphi name, much more self-explanatory.Promiscuous
Actually, prototype is only used with regard to functions in C++, too. C++ uses forward declaration for class types, just like Delphi.Psychrometer
They still mean the same thing to me, just that they decided to use another name later in C++. Or is there a more profound difference I don't know about?Promiscuous
you may look at JavaScript or similar language, it does not have objects inheritance and classes, it uses object prototyping instead. So in general "prototype" term has several meanings and in question like "language X lacks prototypes" would better be avoided. Because in such question just ANY mean of prorotype would fit.Aminopyrine
S
25

Relevant to this question, there are two kinds of inheritance: interface inheritance and implementation inheritance.

Interface inheritance generally implies polymorphism. It means that if B is derived from A, then values of type B can be stored in locations of type A. This is problematic for value types (like records) as opposed to reference types, because of slicing. If B is bigger than A, then storing it in a location of type A will truncate the value - any fields that B added in its definition over and above those of A will be lost.

Implementation inheritance is less problematic from this perspective. If Delphi had record inheritance but only of the implementation, and not of the interface, things wouldn't be too bad. The only problem is that simply making a value of type A a field of type B does most of what you'd want out of implementation inheritance.

The other issue is virtual methods. Virtual method dispatch requires some kind of per-value tag to indicate the runtime type of the value, so that the correct overridden method can be discovered. But records don't have any place to store this type: the record's fields is all the fields it has. Objects (the old Turbo Pascal kind) can have virtual methods because they have a VMT: the first object in the hierarchy to define a virtual method implicitly adds a VMT to the end of the object definition, growing it. But Turbo Pascal objects have the same slicing issue described above, which makes them problematic. Virtual methods on value types effectively requires interface inheritance, which implies the slicing problem.

So in order to properly support record interface inheritance properly, we'd need some kind of solution to the slicing problem. Boxing would be one kind of solution, but it generally requires garbage collection to be usable, and it would introduce ambiguity into the language, where it may not be clear whether you're working with a value or a reference - a bit like Integer vs int in Java with autoboxing. At least in Java there are separate names for the boxed vs unboxed "kinds" of value types. Another way to do the boxing is like Google Go with its interfaces, which is a kind of interface inheritance without implementation inheritance, but requires the interfaces to be defined separately, and all interface locations are references. Value types (e.g. records) are boxed when referred to by an interface reference. And of course, Go also has garbage collection.

Smitten answered 20/1, 2010 at 1:17 Comment(2)
I see now, so basically the problem is mainly with slicing. But wouldn't boxing actually make records exactly like classes, since each variable would actually be a reference to the actual object? Or would that object somehow be actually stack-allocated? Btw, quite the honor to have you answering my question. Brings a smile to my face to have a famous dev answer it. Thank you very much! :)Promiscuous
Boxed records would be, eh, referred to by reference; where the value is stored, whether on the heap or on the stack, is an implementation detail dependent on e.g. escape analysis (does the stack location outlive the reference). But they would not necessarily be just like classes; if the boxed vs non-boxed types had different names or syntaxes, or were like Go and used interface-only inheritance, then one could envision polymorphism for the boxed types (when referred to by a reference) but not for the locations directly (so avoiding slicing). A bit like Java's int / Integer, IOW.Smitten
D
6

Records and Classes/Objects are two very different things in Delphi. Basically a Delphi record is a C struct - Delphi even supports the syntax to do things like have a record that can be accessed as either 4 16bit integers or a 2 32bit integers. Like struct, record dates back to before object oriented programming entered the language (Pascal era).

Like a struct a record is also an inline chunk of memory, not a pointer to a chunk of memory. This means that when you pass a record into a function, you are passing a copy, not a pointer/reference. It also means that when you declare a record type variable in your code, it is determined at compile time how big it is - record type variables used in a function will be allocated on the stack (not as a pointer on the stack, but as a 4, 10, 16, etc byte structure). This fixed size does not play well with polymorphism.

Debroahdebs answered 19/1, 2010 at 17:53 Comment(5)
I know records are the equivalent to structs, but still... that doesn't necessarily exclude expanding them a bit. I'm not quite sure, but I think it is possible to have variant classes as well, so variant records as classes wouldn't pose much of a problem. I know records are stack-allocated vs classes that are heap-allocated, but C++ classes are stack-allocated as well, so I don't see much problem here.Promiscuous
Fixed sizes work fine with polymorphism. Turbo Pascal and C++ both have fixed-size value-type classes that still support inheritance and virtual functions.Psychrometer
@Rob Kennedy: You mean "object" types? They're still present in Delphi, just that they don't have operator overloading, virtual functions (I'm not sure) and have a few problems with properties or anything introduced after them.Promiscuous
Right. They have virtual functions, but they don't support compiler-managed types like interfaces or strings for their fields. They are fundamentally broken in Delphi, so I don't consider them to exist in Delphi in any meaningful way. But their existence in Turbo Pascal proves that it's possible to have value types that support inheritance. (Adding virtual methods to records would be trickier, but virtual methods aren't an essential property of inheritance.)Psychrometer
aren't C++ classes just structs with default visibility clause changed ? At least AFAIR that was the case for early versions of C++Aminopyrine
P
5

The only problem I see (and I could be shortsighted or wrong) is purpose. Records are for storing data while objects are for manipulating and using said data. Why does a storage locker need manipulation routines?

Prickett answered 19/1, 2010 at 17:43 Comment(11)
Yes, but Delphi already has "records with methods", so essentially they can now manipulate data as well. Why not go all the way? Either way, why not have stack-allocated objects, whatever their name? This way, people will stop complaining about lack of garbage collection and all will be wonderful.Promiscuous
The methods you can add to records in Delphi are just syntactic sugar for static methods, just like extension methods in C#. There is no space for polymorphism, as there is no virtual method table being used.Debroahdebs
@Cloud737: All certainly wouldn't be wonderful as long as Delphi doesn't allow for variables to be declared when they are first used.Judejudea
@David: I don't think the virtual method table could be a problem, since it could be implemented, though I'm not sure about the overhead.Promiscuous
@mghie: I... don't quite understand what you meant there.Promiscuous
@Cloud737: Imagine an object on the stack that uses RAII to manage a critical section or other synchronization object. That would be great, as using interfaces for that purpose (as I do now) introduces too much overhead. However, for it to be useful you need to be able to exactly specify when it will be created and destroyed. Possible in C++, not so in Delphi with current rules.Judejudea
Delphi already has "records" with inheritance, VMT and DMT. They are declared by the old "object" keyword. But I wouldn't recommend people to use the old stack based objects because the compiler has some codegen bugs if you try to use code constructs that were introduced after TurboPascal 7.0, e.g. properties.Rafaelle
@Andreas: Yes, but objects of type "object" don't have operator overloading, and I think even virtual methods are missing if I understand right, besides the things you mentioned. What I was thinking was expanding Records so that "object" wouldn't be needed anymore (except for backward compatibility).Promiscuous
@mghie: I think I understand what you mean now. Indeed, Delphi creates all stack objects on entering a function and destroys all on exiting, no exceptions. I'm guessing that a heap object via a pointer is exactly what would be causing the overhead you mentioned (the constant dereferencing), right? I don't see why this would be a problem with making records implement inheritance, since that's how things have been with allocations in Delphi since the beginning of time. :P Also, I think I've read somewhere that being able to declare variables anywhere might cause other ugly problems.Promiscuous
@Cloud737: The dereferencing isn't the biggest problem (often you wouldn't call any methods, leaving the work to constructor and destructor), hitting the memory manager is. Creation of a stack based object just involves manipulating a CPU register to (de-)allocate the memory. Hitting the memory manager is more costly in single-threaded programs, it can be a killer in multi-threaded ones. As for ugly problems, what would they be?Judejudea
@mghie: Ah, so you're basically doing all the work in the constructor and then you destroy it. I was thinking the object might have a bigger life-span, so that you could use it for different cases, as I haven't worked with critical sections or synchronization objects. I understand perfectly what the problem is now. I wish I could remember what those ugly problems were, but unfortunately I never paid much attention. :(Promiscuous
K
5

You're right, adding inheritance to records would essentially turn them into C++ classes. And that's your answer right there: it's not done because that would be a horrible thing to do. You can have stack-allocated value types, or you can have classes and objects, but mixing the two is a very bad idea. Once you do, you end up with all sorts of lifetime-management issues and end up having to build ugly hacks like C++'s RAII pattern into the language in order to deal with them.

Bottom line: If you want a data type that can be inherited and extended, use classes. That's what they're there for.

EDIT: In response to Cloud's question, this isn't really something that can be demonstrated via a single simple example. The entire C++ object model is a disaster. It may not look like one up close; you have to understand several interconnected problems to really grasp the big picture. RAII is just the mess at the top of the pyramid. Maybe I'll write up a more detailed explanation on my blog later this week, if I have the time.

Kafiristan answered 19/1, 2010 at 18:16 Comment(7)
I'm not quite familiar with the problem C++ has that you're referring to. I've not looked at RAII too much. Could you post an example of why it would be a problem, please?Promiscuous
-1 for the misconception that RAII is a necessary but ugly hack.Judejudea
How is it a misconception, mghie? RAII is one of the worst language features I've ever seen, in any programming language, and I'm frankly baffled by how so many C++ coders seem to think it's somehow a good thing.Kafiristan
@Mason: Please, do write a thorough explanation on your blog. If you really have an open mind about things, please read what An­drei Alexan­dres­cu has to write about D (he and Walter Bright, the creator of the D programming language, being extremely bright (no pun intended) people, who could probably code rings around most of the SO participants), and then reflect on the fact that D (while dealing with many of the shortcomings of C++) keeps RAII and the various ways of managing memory in C++, and adds garbage collection on top of it. Maybe, just maybe, you're wrong?Judejudea
Well, my knee-jerk response is to say "of course they did. They had to; they based a lot of their object model on C++'s." But it might be worth a read. I'll check it out. You happen to have a link?Kafiristan
Interface inheritance with value types, i.e. making it possible for different sized value types to be subtypes of one another, so that slicing comes into the picture, is a pretty bad idea. Introduce vtables, and things get worse: you can end up with a type which is too small for some of the methods referred to by its vtable, and you can get buffer overrun problems overwriting the stack. Interface inheritance with value types: just say no.Smitten
@Mason: "The Case for D" (ddj.com/hpc-high-performance-computing/217801225) and the Overview and TOC pages for "The D Programming Language" (my.safaribooksonline.com/9780321659538)Judejudea
H
5

Because records don't have VMT (virtual method table).

Hospice answered 19/1, 2010 at 22:1 Comment(0)
C
4

In times past I have used objects (not classes!) as records with inheritance.

Unlike what some people on here are saying there are legitimate reasons for this. The case I did it involved two structures from external sources (API, not anything off disk--I needed the fully formed record in memory), the second of which merely extended the first.

Such cases are very rare, though.

Cutler answered 19/1, 2010 at 22:55 Comment(1)
These cases are indeed very common, e.g. when using objects like a header + indefinite length data zone.Uvarovite
H
3

You could try to use the Delphi object keyword for that. Those basically are inheritable, but behave much more like records than to classes.

See this thread and this description.

Hallee answered 19/1, 2010 at 22:45 Comment(1)
Ahhh! Nooo! Don't use the object keyword! It's been deprecated for years, and as of D2010, not even really supported properly at all.Cheddar
S
0

This is on topic to your question and relates to extending the functionality of record and class types via class and record helpers. According to Embarcadero's documentation on this you can extend a class or record (but no operator overloading is supported by helpers). So basically you can extend functionality in terms of member methods but no member data). They support class fields which you could access via getters and setters in the usual way though I have not tested this. If you wanted to interface access to the data of the class or record you were adding the helper to, you could probably achieve this (ie triggering an event or signal when the member data of the original class or record was changed). You could not implement data hiding though but it does allow you to override an existing member function of the original class.

eg. This example works in Delphi XE4. Create a new VCL Forms Application and replace code from Unit1 with the following code:

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Types;

type

  TMyArray2D = array [0..1] of single;

  TMyVector2D = record
  public
    function Len: single;
    case Integer of
      0: (P: TMyArray2D);
      1: (X: single;
          Y: single;);
  end;

  TMyHelper = record helper for TMyVector2D
    function Len: single;
  end;


  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;


implementation

function TMyVector2D.Len: Single;
begin
  Result := X + Y;
end;

function TMyHelper.Len: single;
begin
  Result := Sqrt(Sqr(X) + Sqr(Y));
end;

procedure TestHelper;
var
  Vec: TMyVector2D;
begin
  Vec.X := 5;
  Vec.Y := 6;
  ShowMessage(Format('The Length of Vec is %2.4f',[Vec.Len]));
end;

procedure TForm1.Form1Create(Sender: TObject);
begin
  TestHelper;
end;

Notice that the result is 7.8102 rather than 11. This shows that you can hide the member methods of the original class or record with a class or record helper.

So in a way you would just treat access to the original data members just the same as you would in changing values from within the unit in which a class is declared by changing through the properties rather than the fields directly so the appropriate actions are taken by the getters and setters of that data.

Thanks for asking the question. I certainly learned a lot in trying to find the answer and it helped me out a great deal too.

Brian Joseph Johns

Surgy answered 7/10, 2014 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.