www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Why are structs and classes so different?

reply Kevin Bailey <keraba yahoo.com> writes:
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.

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.

A bigger problem is that, if I have a struct that I suddenly want 
to inherit from, I have to change all my code. 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.

Why is it this way? What is the harm of putting a class object on 
the stack?
May 15 2022
next sibling parent reply Alain De Vos <devosalain ymail.com> writes:
Can i summarize ,
structs are value-objects which live on the stack.
class instances are reference objects which live on the heap.
May 15 2022
next sibling parent forkit <forkit gmail.com> writes:
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 15 2022
prev sibling parent reply bauss <jj_1337 live.dk> writes:
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
parent reply Kevin Bailey <keraba yahoo.com> writes:
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
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
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
prev sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/16/22 08:18, Kevin Bailey wrote:

 I was asking, "Why is it this way? Why was it
 designed this way?
I for one misunderstood you. I really thought you were arguing that struct and class should be the same.
 What bad thing happens the other way?"
C++ is proof that it can indeed work the other way. However, for it to work correctly, programmers must follow guidelines. Here are four: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c67-a-polymorphic-class-should-suppress-public-copymove http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c145-access-polymorphic-objects-through-pointers-and-references http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-slice http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#e15-throw-by-value-catch-exceptions-from-a-hierarchy-by-reference C++ does not use terms "value type" and "reference type" to make any distinction but the rules above are proof enough for me that C++ implicitly divides user-defined types into such categories. D is this way because someone realized that there are different kinds of user-defined types.
 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.)
Ok, I think I see better now. You would like members of a class recursively placed next to each other in memory. What if a polymorphic type had a polymorphic member? class Student : Human { double a; Pet pet; double b; } Could Pet (which could be a Cat or a Dog, etc.) fit between a and b? Yes, emplacing the top-level object in our memory would have some value but not all members would be there. For example, D's dynamic arrays are like C++'s vector. What if the type had a vector? It's element would not be on the stack. Granted, the type could have a specific Pet: Dog pet; but I would argue that not all Students would have a Dog.
 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.
I don't understand that example. I see a programmer error of casting a Foo to a Bar.
 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?
I thought more of this and conferred with a colleague who degisned parts of Intel CPUs. He agrees with you: Even if passed by reference, the objects are in CPU cache anyway and the pointer arithmetic is made in the core that all is good. He went further to suggest yes, by-reference will be faster for large structs but there is no clear answer because it all depends on the speeds of the core, cache (including the size), memory.
 D went the Java path: You can have any color you
 like, as long as it's grey.
reference types.
 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.
Not at all!
 I've learned from all the projects that I've been on
 that we need all these types.
With all due respect, based on other conversation here, may I assume you those projects were based on C++? If you also had any language with reference types like Python, did you have the similar issues with those languages? I accepted D's struct/class separation since the beginning and never had any issue with it. It just worked for me. Ali
May 16 2022
parent reply Kevin Bailey <keraba yahoo.com> writes:
Hi again Ali!

On Monday, 16 May 2022 at 21:58:09 UTC, Ali Çehreli wrote:
 I for one misunderstood you. I really thought you were arguing 
 that struct and class should be the same.
To be specific: - My *primary* concern is that: Foo foo; is undefined behavior waiting to happen, that I can't detect at a glance. - A *secondary* goal would be for class objects to be able to have deterministic destructors like structs. The first looks trivial: Just have it allocate a Foo by default. The second looks like it could have been designed that way, albeit with other minor changes to the language, and, I was curious why it wasn't.
 C++ is proof that it can indeed work the other way. However, 
 for it to work correctly, programmers must follow guidelines. 
 Here are four:
But again, from this discussion, it seems D could have simply had pass-by-reference for class objects, preserving everything D strives for, but by-value stack initialization, providing what people expect from stack objects. There's no need to drag C++ design into it.
 C++ does not use terms "value type" and "reference type" to 
 make any distinction but the rules above are proof enough for 
 me that C++ implicitly divides user-defined types into such 
 categories.
I would beg to differ here. In C++, all types are value types, until you add punctuation. This is one of the things that I like about C++, that I trip over in other languages. e.g. In Rust, there are 2 similar types, is it int and float, where one is copyable and the other move-only? How can you write generic code in that environment? Yes, in C++, you have to worry about slicing and copying singletons, but these are problems in front of you. It's the problems that sneak up behind you that I worry about.
 Ok, I think I see better now. You would like members of a class 
 recursively placed next to each other in memory. What if a 
 polymorphic type had a polymorphic member?
You mean like a string? I don't have a problem with this: class MyString { uint length; ...pointer to data... } void func() { MyString s; if (s.length == 0) // I want this to be perfectly safe. writeln("empty"); // 's' destroyed here, could do something useful } In D, some objects can do this; some can't!
 I don't understand that example. I see a programmer error of 
 casting a Foo to a Bar.
Correct, I was responding to a comment. I was pointing out that the only "slicing" that we need to worry about with by-reference is if we are already doing something wrong, and that D won't help you there.

 classes are reference types.
You missed the part where I said, "ignoring structs". :-)
 With all due respect, based on other conversation here, may I 
 assume you those projects were based on C++? If you also had 
 any language with reference types like Python, did you have the 
 similar issues with those languages?
Python is a toy language, right? I'm not aware of any large projects in it. (The largest I worked with was 138 files, 31k lines - tiny.) Java would be a better comparison, but it has auto-closable objects, unlike D and Python. Perhaps it has succeeded because there are so few types that *aren't* by reference. Perhaps one just gets used to it. (I've written Android apps, but I would never write a long-running service in it.) This is unfortunate for D where you have to keep track.
 I accepted D's struct/class separation since the beginning and 
 never had any issue with it. It just worked for me.
Perhaps you are familiar with the types that you work with on a daily basis. Perhaps the project is small. Perhaps your IDE colors classes brightly. I don't know, but an anecdote doesn't mean much compared to the fact that nearly all large projects are in C++, and I don't mean "due to inertia." I mean, "and they're successful."
May 17 2022
next sibling parent Adam D Ruppe <destructionator gmail.com> writes:
On Tuesday, 17 May 2022 at 14:40:48 UTC, Kevin Bailey wrote:
 Foo foo;

 is undefined behavior waiting to happen, that I can't detect at 
 a glance.
It is actually perfectly well defined - for the class, it will be null, and this will kill the program if you use it. You might not like that definition, but that's how it is defined.
 - A *secondary* goal would be for class objects to be able to 
 have deterministic destructors like structs.
they can with the `scope` keyword.
 The first looks trivial: Just have it allocate a Foo by default.
Worth noting that D *never* calls a user defined function on variable declaration; there are no default constructors. Even if it is a struct, it never actually calls a constructor, it just copies over the default init value. This is different than classes, which have some kind of constructor call any time you make one. I think it is probably this default constructor stance that the rest flows from: a class is assumed to encapsulate some non-trivial state so it has a constructor, interfaces, object identities, etc. A struct is more of a plain collection of data. Of course, in practice the lines are more blurred than that, but I think that's where it comes from. Especially if you look at the older D versions when structs didn't support constructors at all.
May 17 2022
prev sibling parent =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/17/22 07:40, Kevin Bailey wrote:

 Foo foo;

 is undefined behavior waiting to happen, that I can't detect at a glance.
Foo is null there. (I don't remember whether accessing through null reference is undefined or segmentation fault (on common systems).) Using foo will not do some random thing.
 - A *secondary* goal would be for class objects to be able to have
 deterministic destructors like structs.
I understand. D uses garbage collection by default for classes. Having this choice of struct versus class is useful. This has been mentioned before: structs are the predominant user-defined type in D. Classes are only for polymorphic behavior.
 C++ does not use terms "value type" and "reference type" to make any
 distinction but the rules above are proof enough for me that C++
 implicitly divides user-defined types into such categories.
I would beg to differ here. In C++, all types are value types, until you add punctuation. This is one of the things that I like about C++, that I trip over in other languages. e.g. In Rust, there are 2 similar types, is it int and float, where one is copyable and the other move-only? How can you write generic code in that environment? Yes, in C++, you have to worry about slicing and copying singletons, but these are problems in front of you. It's the problems that sneak up behind you that I worry about.
Yes, that's where we differ. :) You see problems that can sneak up, I see problems disappeared.
 Ok, I think I see better now. You would like members of a class
 recursively placed next to each other in memory. What if a polymorphic
 type had a polymorphic member?
You mean like a string? I don't have a problem with this: class MyString { uint length; ...pointer to data... } void func() { MyString s;
It is easy to adapt to the following syntax: auto s = /* some expression */; auto s = new MyString(); auto s = scoped!MyString(); scope s = new MyString(); MyString could have been a member variable, which would be initialized in a constructor (or not; everything gets the .init value).
     if (s.length == 0)  // I want this to be perfectly safe.
        writeln("empty");
     // 's' destroyed here, could do something useful
Such types are rare in my experience especially because of the GC. Once you remove explicit freeing of memory, most destructors disappear as well. In cases where deterministic destruction is needed, we have a number of options all of which were used by me: - scope has already been mentioned. - RAII: MyString could be owned by a struct (e.g. MyStringCloser) either for every use or wherever it makes sense. - scope (exit) { s.close(); }
 }

 In D, some objects can do this; some can't!
Perhaps in a template, yes. Then we may have to do some introspection there: static if (is (T == class)) { auto s = new T(); } else { auto S = T(); } I've seen similar concerns raised about this difference before. Just checked: Yes, there are 45 occurrences of that check under /usr/include/dlang/dmd on my system, which includes Phobos and core.
 Python is a toy language, right? I'm not aware of any large projects in
 it. (The largest I worked with was 138 files, 31k lines - tiny.)
Wow! That's way beyond my pay grade. :) But I have a feeling you will like D despite its differences.
 Perhaps the project is small.
Correct. I would be happy if others chimed in but this struct/class difference is not a common issue with D at all.
 Perhaps your IDE colors classes
 brightly.
Yes, I use syntax highlighting with muted colors. :) I still use Emacs with a suboptimal configuration. (I need to work on fixing that already. :/) Ali
May 17 2022
prev sibling next sibling parent Guillaume Piolat <first.last gmail.com> writes:
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.
Perhaps someone more informed will chime in, but there is a reason to avoid object inheritance with value types, and force them to be reference types. https://stackoverflow.com/questions/274626/what-is-object-slicing If we want to avoid that problem, then object with inheritance and virtual functions have to be reference types. But you still need values types. So now you have both struct and For an escape hatch, D has library ways to have structs with virtual functions (there is a DUB package for that), and classes on the stack (Scoped!T, RefCounted!T, a __traits).
May 15 2022
prev sibling next sibling parent reply Mike Parker <aldacron gmail.com> writes:
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: ```d 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](https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-a d-the-gc-part-one/) 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.
May 15 2022
next sibling parent reply Kevin Bailey <keraba yahoo.com> writes:
Hi Mike (and Guillaume, since you posted the same link),

Thanks for the long explanation.

I've been programming in C++ full time for 32 years, so I'm 
familiar with slicing. It doesn't look to me like there's a 
concern here.

There seem to be a couple different questions here. I suspect 
that you answered a different one than I asked.

One question is, how should we pass objects - by value or by 
reference? In C++, you can do either, of course, but you take 
your chances if you pass by value - both in safety AND 
PERFORMANCE. The bottom line is that no one passes by value, even 
for PODs (although we may return even large objects.)

But I asked a different question: Why can't I put a class object 
on the stack? What's the danger?

Note that operating on that object hasn't changed. If I pass by 
reference, it's no different than if I had created a reference.

One might say, Well, if D creates by value, then it has to pass 
by value. But it doesn't; it has the 'ref' keyword.

One might want to avoid passing by value accidentally. Ok, one 
could have D pass class objects by reference implicitly.

I don't like things silently changing like that, so one might 
have D forbid all but pass by 'ref' or pointer for class objects.

In any case, this doesn't quite address the instantiate by value 
issue.

If there's still a case where putting an object on the stack 
breaks, I would greatly appreciate seeing a few lines of example 
code.

I hope Ali's answer isn't the real reason. I would be sad if D 
risked seg faults just to make class behavior "consistent".

thx
May 15 2022
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Sunday, 15 May 2022 at 20:05:05 UTC, Kevin Bailey wrote:
 I've been programming in C++ full time for 32 years, so I'm 
 familiar with slicing. It doesn't look to me like there's a 
 concern here.
Yes, slicing is not the issue. Slicing is a problem if you do "assignments" through a reference that is typed as the superclass… so references won't help. The original idea might have been that structs were value-types, but that is not the case in D. They are not, you can take the address… So what you effectively have is that structs follow C layout rules and D classes are not required to (AFAIK), but there is an ABI and C++ layout classes, so that freedom is somewhat limited… D classes also have a mutex for monitors. In an ideal world one might be tempted to think that classes were ideal candidates for alternative memory management mechanisms since they are allocated on the heap. Sadly this is also not true since D is a system level programming language and you get to bypass that "type characteristic" and can force them onto the stack if you desire to do so…
May 15 2022
prev sibling next sibling parent Mike Parker <aldacron gmail.com> writes:
On Sunday, 15 May 2022 at 20:05:05 UTC, Kevin Bailey wrote:

 One question is, how should we pass objects - by value or by 
 reference? In C++, you can do either, of course, but you take 
 your chances if you pass by value - both in safety AND 
 PERFORMANCE. The bottom line is that no one passes by value, 
 even for PODs (although we may return even large objects.)
Pass struct instances by ref or by value as needed, just as you do in C++. For classes, you never have direct access to the instance. Your class reference is a handle (a pointer) that is always passed by value.
 But I asked a different question: Why can't I put a class 
 object on the stack? What's the danger?
I answered that one. You can put a class on the stack with `scope`. There is no danger in that. If you're wanting direct access to the class instance, like you would have with a struct, you don't have that in D. Classes are modeled on Java, not C++.
 Note that operating on that object hasn't changed. If I pass by 
 reference, it's no different than if I had created a reference.
Again, you never have direct access to the object instance. You always access it through the handle.
 One might say, Well, if D creates by value, then it has to pass 
 by value. But it doesn't; it has the 'ref' keyword.
Everything is passed by value unless the `ref` keyword is present.
 One might want to avoid passing by value accidentally. Ok, one 
 could have D pass class objects by reference implicitly.
How do you pass by value accidentally? By forgetting the `ref` keyword?
 I don't like things silently changing like that, so one might 
 have D forbid all but pass by 'ref' or pointer for class 
 objects.
I don't understand where you're coming from here. How can things silently change?
 I hope Ali's answer isn't the real reason. I would be sad if D 
 risked seg faults just to make class behavior "consistent".
Where is the risk of seg faults? Are you referring to the fact that class references are default initialized to null?
May 15 2022
prev sibling next sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/15/22 13:05, Kevin Bailey wrote:

 I've been programming in C++ full time for 32 years
Hi from an ex-C++'er. :) I managed to become at least a junior expert in C++ between 1996-2015. I don't use C++ since then. I still think my answer is the real one. My implied question remains: Why does C++ have struct and class disticnction? I know they have different default access specifications but does that warrant two kinds? I claim there are two types in C++ as well: value types and reference types. And types of an inheritance hirerachy are by convention reference types. As others reminded on this thread, C++ programmers follow guidelines to treat types of hierarchies as reference types.
 so I'm familiar
 with slicing. It doesn't look to me like there's a concern here.
Slicing renders types of class hierarchies reference types. They can't be value types because nobody wants to pass a Cat sliced as an Animal. It's always a programmer error. these two kinds and utilize existing keywords.
 One question is, how should we pass objects - by value or by reference?
 In C++, you can do either, of course, but you take your chances if you
 pass by value - both in safety AND PERFORMANCE.
D is very different from C++ when it comes to that topic: - Since classes are reference types, there is no issue with performance whatsoever: It is just a pointer copy behind the scenes. - Since structs are value types, they can be shallow-copied without any concern. (D disallows self-referencing structs.) Only when it matters, one writes the copy constructor or the post-blit. (And this happens very rarely.) - rvalues are moved by default. They don't get copied at all. (Only for structs because classes don't have rvalues.)
 The bottom line is that
 no one passes by value, even for PODs (although we may return even large
 objects.)
I know it very well. In reality, nobody should care unless it matters semantically: Only if the programmer wants to pass an object by reference it should be done so. For example, to mutate an object or store a reference to it. You must be familiar with the following presentation by Herb Sutter how parameter passing is a big problem. (Yet, nobody realizes until a speaker like Herb Sutter makes a presentation about it.) https://www.youtube.com/watch?v=qx22oxlQmKc&t=923s Such concerns don't exist in D especially after fixing the "in parameters" feature. Semantically, the programmer should say "this is an input to this function". The programmer should not be concerned whether the number of bytes is over a threshold for that specific CPU or twhether the copy constructor may be expensive. D does not have such issues. The programmer can do this: - Compile with -preview=in - Mark function parameters as in (the ones that are input): auto foo(in A a, in B b) { // ... } The compiler should deal with how to pass parameters. The programmer provides the semantics and D follows these rules: https://dlang.org/spec/function.html#in-params Although one of my colleagues advices me to not be negative towards C++, having about 20 years of experience with C++, I am confident C++ got this wrong and D got this right. D programmers don't write move constructors or move assignment. Such concepts don't even exist. In summary, if a programmer has to think about pass-by-reference, that programmer has been conditioned to think that way. It has always been wrong. Passing by reference should have been about semantics. (Herb Sutter uses the word "intent" in that presentation.)
 But I asked a different question: Why can't I put a class object on the
 stack? What's the danger?
There is no danger. One way I like is std.typecons.scoped: import std.stdio; import std.typecons; class C { ~this() { writeln(__FUNCTION__); } } void main() { { auto c = scoped!C(); } writeln("after scope"); }
 Note that operating on that object hasn't changed. If I pass by
 reference, it's no different than if I had created a reference.
(Off-topic: I always wonder whether pass-by-reference comes with performance cost. After all, the members of by-reference struct will have to be accessed through a pointer, right? Shouldn't pass-by-value be faster for certain types? I think so but I never bothered to check the size threshold below which to confidently pass-by-value.)
 One might say, Well, if D creates by value, then it has to pass by
 value. But it doesn't; it has the 'ref' keyword.
That's only when one wants to pass a reference to an object. I blindly pass structs by-value. The reason is, I don't think any struct is really large to cost byte copying. It's just shallow copy and it works. (Note that there are not much copy constructors in D.)
 I hope Ali's answer isn't the real reason. I would be sad if D risked
 seg faults just to make class behavior "consistent".
I don't understand the seg fault either but my answer was to underline the fact that D sees two distinct kinds of types: value types and reference types. C++ does have reference types as well but they are implied by convention. Otherwise the programmer hits the slicing issue. Ali
May 15 2022
next sibling parent reply Tejas <notrealemail gmail.com> writes:
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 15 2022
parent =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/15/22 21:20, Tejas wrote:

 Never say never :

 https://github.com/dlang/DIPs/blob/master/DIPs/DIP1040.md
Thanks! I am reading it now.
 Also, there's `opPostMove` already within the language
I see: It indeed appears on some pages on dlang.org but the language spec has no mention of it. :) It looks like a DbI (design by introspection) thing in Phobos or core. It looks like structs with self references are allowed now as long as they do the right thing in their opPostMove. Cool... Ali
May 16 2022
prev sibling parent reply forkit <forkit gmail.com> writes:
On Sunday, 15 May 2022 at 21:33:24 UTC, Ali Çehreli wrote:
 ....
 I still think my answer is the real one. My implied question 
 remains: Why does C++ have struct and class disticnction? I 
 know they have different default access specifications but does 
 that warrant two kinds?
 ....
Here is a very interesting article that researches this subject. https://belaycpp.com/2021/09/17/history-of-c-explanation-on-why-the-keyword-class-has-no-more-reason-to-exist/
May 18 2022
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Wednesday, 18 May 2022 at 10:53:03 UTC, forkit wrote:
 Here is a very interesting article that researches this subject.
Yeah, he got it right. It is syntax sugar that makes verbose C++ code easier to read. Use struct for internal objects and tuple like usage and class for major objects in your model. But Simula used class for more: coroutines and library modules. So, you could take a class and use its scope in your code and thereby get access to the symbols/definitions in it. The successor to Simula, called Beta, took it one step further and used the class concept for functions, block scopes, loops, almost everything. The language Self went even further and collapsed the concept of class and object into one... perhaps too far... People prefer Typescript over Javascript for a reason. 😁
May 18 2022
prev sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Sun, May 15, 2022 at 08:05:05PM +0000, Kevin Bailey via Digitalmars-d-learn
wrote:
[...]
 But I asked a different question: Why can't I put a class object on
 the stack? What's the danger?
[...] You can. Use core.lifetime.emplace. Though even there, there's the theoretical problem of stack corruption: if you have an emplaced class object O and you try to assign a derived class object to it, you could end up trashing your stack (the derived object doesn't fit in the stack space allocated to store only a base class instance). Generally, though, the language would prevent this. In D this doesn't happen because emplace just gives you the class instance as a reference (to a stack location, but nonetheless), and reassignment just updates the reference, it doesn't actually overwrite the base class instance. T -- If it tastes good, it's probably bad for you.
May 15 2022
prev sibling parent reply IGotD- <nise nise.com> writes:
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
parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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
prev sibling next sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
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. Ali
May 15 2022
parent reply Johan <j j.nl> writes:
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
May 16 2022
next sibling parent reply Alain De Vos <devosalain ymail.com> writes:
A new syntax like "*" should introduce something new.
If it's not needed for classes why introduce it.
If you don't know if something is a class name it class_blabla.
Just remember the effect of "="
May 16 2022
parent reply Kevin Bailey <keraba yahoo.com> writes:
On Monday, 16 May 2022 at 19:06:01 UTC, Alain De Vos wrote:
 A new syntax like "*" should introduce something new.
 If it's not needed for classes why introduce it.
Hi Alain! I have to sympathize with Johan. If you see: Foo foo = get_foo(); call_function(foo); can 'foo' change in 'call_function()' ? Is it by-reference or is it by value? Foo* foo = get_foo(); How about now? Pretty obvious. call_function(&foo); Also obvious. To re-phrase your claim, a new syntax doesn't need to introduce something new. Syntax is there to convey information.
 If you don't know if something is a class name it class_blabla.
 Just remember the effect of "="
ah, I see the problem. You've never worked in a large code-base written by thousands of other people. I do every day. I can't make them name things in any special way. But when I see the above code, I need to know exactly what it does with just a scan.
May 16 2022
parent =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/16/22 13:48, Kevin Bailey wrote:

 a large code-base written
 by thousands of other people. I do every day. I can't make them name
 things in any special way.
I think we need a comparable D project to know whether this is really an issue.
 But when I see the above code, I need to know
 exactly what it does with just a scan.
Most IDEs and editors show whether a type is a class or a struct. Ali
May 16 2022
prev sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 5/16/22 10:35, Johan wrote:

 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.
I see. Is it really a high mental load without the syntax? I seems to just work in my programs but perhaps because I am the main programmer and classes are very rare anyway. Also, same syntax is said to help with template code but perhaps the argument there is a template must be written either for value types or reference types? I am not sure.
 A few years ago when I taught C++, this was 50% of the reason for me not
 to teach D.
That's unfortunate. :(
 I see a big confirmation of that decision in this thread.
Luckily, in my experience such threads are not frequent. Still, we may see a related topic at DConf. ;) Ali
May 16 2022
parent Johan <j j.nl> writes:
On Monday, 16 May 2022 at 21:20:43 UTC, Ali Çehreli wrote:
 On 5/16/22 10:35, Johan wrote:

 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.
I see. Is it really a high mental load without the syntax? I seems to just work in my programs but perhaps because I am the main programmer and classes are very rare anyway. Also, same syntax is said to help with template code but perhaps the argument there is a template must be written either for value types or reference types? I am not sure.
`Foo a = b;` What does that do? A benefit of statically typed languages is that you know what simple code will do. But for this simple statement, you actually don't know. Perhaps it is a copy, perhaps not. I understand that also structs can have reference-like semantics with copies if they contain pointers, but it is strange that the language at a fundamental basic level has this ambiguity of user types. Indeed the same-syntax template argument is bogus, for exactly this reason that you don't know what the code is doing. If the language deems it important enough to separate value types from reference types, then why does it allow passing _both_ to `foo(T)(T t)` ? -Johan
May 17 2022
prev sibling next sibling parent forkit <forkit gmail.com> writes:
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
prev sibling parent Walter Bright <newshound2 digitalmars.com> writes:
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 15 2022