May 16, 2022
On Sunday, 15 May 2022 at 15:59:17 UTC, Alain De Vos wrote:
> Can i summarize ,
> structs are value-objects which live on the stack.
> class instances are reference objects which live on the heap.

the real difference, is that structs, being value types, are passed by value, and classes, being reference types, are passed by reference.

this is the most important difference to be aware of.

where they live in memory should be less of the programmers concern, and more an implementation issue (although some programmers will of course consider this as well).

btw. where does a struct, inside a class live?

May 16, 2022
On Sunday, 15 May 2022 at 15:26:40 UTC, Kevin Bailey wrote:
> I've done some scripting in D over the years but I never dug into D until recently. I'm going through Learning D and I was reminded that structs and classes are so different.
>
> - struct methods are non-virtual while class methods are virtual
> - Thus, structs can't inherit, because how would you find the child's destructor given a parent pointer?
> - On the stack, structs by-value but classes are by-reference
>
> 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.

A virtual function call has to pass through a virtual function look-up, and thus there is (some) overhead involved.

Thus, by design, structs avoid this overhead (completely).

It's also (I think) another reason why D does not support multiple inheritance. Since you would need multiple virtual function tables.
May 15, 2022
On 5/15/2022 8:26 AM, Kevin Bailey wrote:
> I'm trying to understand why it is this way.
Great question.

The difference, in a nutshell, is a struct is a value type, and a class is a reference type. This difference permeates every facet their behavior.

In C++, a struct can designed to be a value type or a reference type. But C++ does not recognize the difference, and so you can pass a reference type by value, which leads to all sorts of problems. You'll see C++ structs confused about what they are, as the designer didn't know the difference and would put a foot in both camps. A common example of this confusion is putting in virtual functions but neglecting to make the destructor virtual.

D draws a hard distinction between the two, making it both self-documenting, and heading off all sorts of errors from misusing one as the other.

A reference type is inherently a polymorphic type (i.e. virtual functions). Polymorphism via inheritance makes no sense for a value type.

Copy constructors make no sense for a polymorphic type, but are sensible for a value type.

And so on.

A strong distinction between value and reference types has turned out well for D. Naturally, some people still want a type to be both a floor wax and a dessert topping, but D is purposely going to make it difficult to do that.

P.S. Yes, you can pass a struct by reference with the `ref` keyword. That's not polymorphic behavior, though.

P.P.S. Yes, you can allocate a class instance on the stack rather than the GC by using the `scope` storage class. It will still be a reference type, but the compiler won't allow that reference to live longer than its stack frame. Java will automagically allocate classes on the stack if it can determine it cannot escape.
May 16, 2022

On Sunday, 15 May 2022 at 21:33:24 UTC, Ali Çehreli wrote:

>

D programmers don't write move constructors or move assignment. Such concepts don't even exist.

Never say never :

https://github.com/dlang/DIPs/blob/master/DIPs/DIP1040.md

Walter is one of the authors of the DIP

Also, there's opPostMove already within the language

May 16, 2022
On Sunday, 15 May 2022 at 15:59:17 UTC, Alain De Vos wrote:
> Can i summarize ,
> structs are value-objects which live on the stack.
> class instances are reference objects which live on the heap.

But that's not entirely true as you can allocate a struct on the heap as well.

The real difference is inheritance and polymorphism, not allocation and where the memory lives.
May 16, 2022
Great responses, everyone. I'll try to address all of them.

Mike, I know the rules. I was asking, "Why is it this way? Why was it designed this way? What bad thing happens the other way?" When I think about most things in D, I can at least think of a reason, even if I don't agree with it. But not putting class objects on the stack makes no sense to me (assuming we still pass by reference.) Reasons below.

Ola, your response is very interesting. I would say that assignment isn't any more or less of an issue, as long as you pass by reference:

// Using C syntax to make intent clear.
class Foo { int x; }
class Bar: Foo { int y; }

void func1(Bar* bar) {
  bar.y = 4; // boom
}

void func2(Bar* bar) {
  Foo* foo = bar;
  foo.x = 3; // this is fine

  foo = new Foo;
  func1(cast(Bar*)(foo)); // uh oh
}

Illuminating comment about the ABI.

Ali, I've never liked the distinction between 'struct' and 'class' in C++ either, but that's no reason to actually make them different. It's a reason to remove 'class' and save the keyword.

re: pass-by-reference and performance, agreed, this is why we pass integers in registers. But a struct on the stack is just as fast to access locally through a pointer register - "[bp+6]" - as remotely, yes?

Finally, 'scope' is nice but it doesn't solve the segfault issue.

HS Teoh: See above for my responses about assignment and 'scope'.

bauss: "But that's not entirely true as you can allocate a struct on the heap as well."

forkit: "where they live in memory should be less of the programmers concern, and more an implementation issue (although some programmers will of course consider this as well)."

Precisely. You can allocate structs, and you can put class objects in 'scope' variables. (I'm not sure if this was your intent, forkit, but) a class object can live on the stack just as easily as on the heap, as long as you pass by reference. The only danger is if a called function tries to own a stack allocated object, but this is a concern for heap allocated objects too. This is why C++ has moveable types and unique_ptr.

Walter, Thanks for the insightful reply! I'm getting the sense that the decision was made in order to make the language simpler. That is, ignoring struct's, D went the Java path: You can have any color you like, as long as it's grey.

I agree that C++'s organic growth allows programmers to do things they shouldn't, and requires that they be more careful. But I would not have gone from there to Java.

I think an interesting middle-ground would be a language that had "template" types - Copyable, MoveOnly, Interface, Singleton, FactoryBuilt, etc. I've learned from all the projects that I've been on that we need all these types. We can either ask programmers to hand-craft them, or we can provide them. Note that in C++, we can get pretty close:

class Foo: Moveable<Foo> { ...

And then there's the segfault issue. I think that, if we're going to ignore a huge problem like that, there has to be very strong reasons. From this discussion, it doesn't sound like they were very strong.

Of course, it's done and there's little changing it. A seemingly harmless fix would be to not require 'new':

Foo foo; // this allocates an object
Foo foo = null; // this does not
Foo foo = function(); // this doesn't either

In fact, I suspect we could make that change today and few would notice. Nevertheless, I'm still a little shocked that this isn't existing behavior.

cheers all

May 16, 2022

On Monday, 16 May 2022 at 15:18:11 UTC, Kevin Bailey wrote:

>

I would say that assignment isn't any more or less of an issue, as long as you pass by reference:

What I mean is if you write your code for the superclass, and later add a subclass with some invariants that depends on the superclass fields… then upcast an instance of the subclass to the superclass and pass it on… your risk the same issue. The subclass invariant can be broken because of sloppy modelling.

The premise for "slicing" being an issue is that people who write functions have no clue about how the type system works or how to properly model. So it is a lot of fuzz over nothing, because with that assumption you can make the exact same argument for references. (I've never had any practical issues related to "slicing", ever…)

Besides, slicing can very well be exactly what you want, e.g. if you have a super-class "EntityID" and want to build an array of all the EntityIDs… nothing wrong about slicing out the EntityID for all subclass instances when building that array.

Now, there are many other issues with C++, mostly related to the fact that they give very high priority to avoid overhead. E.g. take a new feature like std::span, if you create a subspan ("slice" in D terminology) and the original span does not contain enough elements then C++ regards that as undefined behaviour and will happily return a span into arbitrary memory past the end of the original span. C++ is very unforgiving in comparison to "higher level" languages like D.

If we extend this reasoning to D classes, one can say that D classes are convenience constructs that does not pay as much attention to overhead. One example of this is how interfaces are implemented, each interface will take a full pointer in every instance of the class. The monitor mutex is another example. And how pointers to classes are different (simpler syntax) than pointers to struct also suggests that classes are designed more for convenience than principles.

Whether this is good or bad probably depends on the user group:

  1. Those that are primarily interested in low level with a bit of high level might think it is "too much" and favour structs.

  2. Those that are primarily interested in high level with a bit of low level might think otherwise.

In C++ everyone belong to group 1. In other system languages such as D and Rust you probably have a large silent majority in group 2. (All those programmers that find C++ to be too brittle or hard to get into, but want comparable performance.)

May 16, 2022

On Sunday, 15 May 2022 at 16:08:01 UTC, Mike Parker wrote:

>

scope in a class variable declaration will cause it to the class to be allocated on the stack.

Common practice is that a class has class members itself. So where are they allocated? Most likely is only the top class that is on the stack, the class members are allocated on the heap because the constructor is already compiled.

That scope isn't that useful unless you have it like C++, that expands class members in the parent class.

May 16, 2022
On Mon, May 16, 2022 at 05:02:57PM +0000, IGotD- via Digitalmars-d-learn wrote:
> On Sunday, 15 May 2022 at 16:08:01 UTC, Mike Parker wrote:
> > 
> > `scope` in a class variable declaration will cause it to the class to be allocated on the stack.
> > 
> 
> Common practice is that a class has class members itself. So where are they allocated? Most likely is only the top class that is on the stack, the class members are allocated on the heap because the constructor is already compiled.
> 
> That scope isn't that useful unless you have it like C++, that expands class members in the parent class.
[...]

C++ embedded class members suffer from the same problems as by-value class objects. I.e., object truncation when you assign a derived class member to it.  The only way to avoid this problem in C++ is to turn them into pointers, at which point it becomes equivalent to D class members that by default are reference types.

In D, if you have members that you want to have by-value semantics, just use structs instead.  In general, in my own D code I rarely use classes. Structs are my go-to constructs; only when there is good reason I use classes -- usually when I need inheritance, which is also when by-value types would encounter truncation issues.  Since it *is* possible to pass around pointers to structs, I don't really see much reason for using classes if you don't need inheritance, i.e., when you'll never run into truncation issues.

So IMO D's design of structs and classes makes much more sense than in C++, where `struct` and `class` means essentially the same thing (just with some different default protections -- just lip gloss, really), and where the unclear intention of whether you want a by-value or by-reference type means that truncation issues keep cropping up. D's choice to settle this decision and bake it into the language IMO was the right choice. (Now obviously, this implies that the usage of structs / classes will differ between D and C++... but then again, that's why this is D, not C++. I want to speak idiomatic D, not D with a C++ lisp. :-P)


T

-- 
The richest man is not he who has the most, but he who needs the least.
May 16, 2022

On Sunday, 15 May 2022 at 16:36:05 UTC, Ali Çehreli wrote:

>

On 5/15/22 08:26, Kevin Bailey wrote:

>

structs and classes are so different.

I think a more fundamental question is why structs and classes both exist at all. If they could be the same, one kind would be sufficient. And the answer is there are value types and there are reference types in programming.

What is very problematic is that you cannot see the difference in syntax. In my opinion it would have been much better if the language required using a * for class types: for example Foo* a, and Foo a would simply give a compile error.
A few years ago when I taught C++, this was 50% of the reason for me not to teach D. I see a big confirmation of that decision in this thread.

-Johan