On Sunday, 15 May 2022 at 15:26:40 UTC, Kevin Bailey wrote:
>I'm trying to understand why it is this way. I assume that there's some benefit for designing it this way. I'm hoping that it's not simply accidental, historical or easier for the compiler writer.
There's a problem that arises with pass-by-value subclasses called "object slicing". Effectively, it's possible to "slice off" members of superclasses. It's one of many pitfalls to be avoided in C++. There you typically find the convention of structs being used as POD (Plain Old Data) types (i.e., no inheritance) and classes when you want inheritance, in which case they are passed around to functions as references.
For reference:
https://stackoverflow.com/questions/274626/what-is-object-slicing
D basically bakes the C++ convention into the language, with a class system inspired by Java.
>One problem that this causes is that I have to remember different rules when using them. This creates the additional load of learning and remembering which types are which from someone else's library.
Of all the complexity we need to remember as programmers, this is fairly low on the totem pole. Simple rule: if you need (or want) inheritance, use classes. If not, use structs.
>A bigger problem is that, if I have a struct that I suddenly want to inherit from, I have to change all my code.
You should generally know up front if you need inheritance or not. In cases where you change your mind, you'll likely find that you have very little code to change. Variable declarations, sure. And if you were passing struct instances to functions, you'd want to change the function signatures, but that should be the lion's share of what you'd need to change. After all, D doesn't use the ->
syntax for struct pointers, so any members you access would be via the dot operator.
In addition to that work, in both of these cases, one could easily do it wrong:
// Fine with a struct, fatal with a class.
Foo foo;
At least in C++, the compiler would complain. In D, not even a warning.
You generally find out about that pretty quickly in development, though. That's a good reason to get into the habit of implementing and running unit tests, so if you do make changes and overlook something like this, then your tests will catch it if normal operation of the program doesn't.
>Why is it this way? What is the harm of putting a class object on the stack?
I've answered the "why" above. As to the the second question, there's no harm in putting a class on the stack:
import std.stdio;
class Clazz {
~this() { writeln("Bu-bye"); }
}
void clazzOnStack() {
writeln("Entered");
scope c = new Clazz;
writeln("Leaving");
}
void main()
{
clazzOnStack();
writeln("Back in main");
}
You'll find here that the destructor of c
in clazzOnStack
is called when the function exits, just as if it were a struct. scope
in a class variable declaration will cause it to the class to be allocated on the stack.
Note, though, that c
still a reference to the instance. You aren't manipulating the class instance directly. If you were to pass c
to a function doSomething
that accepts a Clazz
handle, it makes no difference that the instance is allocated on the stack. doSomething
would neither know nor care. c
is a handle, so you aren't passing the instance directly and it doesn't matter where it's allocated.
There's more to the story than just reference type vs. value type. Structs have deterministic destruction, classes by default do not (scope
can give it to you as demonstrated above). See my blog post on the topic for some info. (And I'm reminded I need to write the next article in that series; time goes by too fast).
Everyone has their own criteria for when to choose class and when to choose struct. For me, I default to struct. I consider beforehand if I need inheritance, and if yes, then I ask myself if I can get by without deterministic destruction. There are ways to simulate inheritance with structs, and ways to have more control over destruction with classes, so there are options either way.