Why can't Delphi records have inheritance?

Viewed 7305

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?

8 Answers

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.

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.

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?

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.

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

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.

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.

Related