digitalmars.D.learn - Why are structs and classes so different?
- Kevin Bailey (24/24) May 15 2022 I've done some scripting in D over the years but I never dug into
- Alain De Vos (3/3) May 15 2022 Can i summarize ,
- forkit (9/12) May 15 2022 the real difference, is that structs, being value types, are
- bauss (5/8) May 16 2022 But that's not entirely true as you can allocate a struct on the
- Kevin Bailey (71/71) May 16 2022 Great responses, everyone. I'll try to address all of them.
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (41/43) May 16 2022 What I mean is if you write your code for the superclass, and
- =?UTF-8?Q?Ali_=c3=87ehreli?= (52/85) May 16 2022 I for one misunderstood you. I really thought you were arguing that
- Kevin Bailey (59/81) May 17 2022 To be specific:
- Adam D Ruppe (18/24) May 17 2022 It is actually perfectly well defined - for the class, it will be
- =?UTF-8?Q?Ali_=c3=87ehreli?= (41/76) May 17 2022 Foo is null there. (I don't remember whether accessing through null
- Guillaume Piolat (12/16) May 15 2022 Perhaps someone more informed will chime in, but there is a
- Mike Parker (69/87) May 15 2022 There's a problem that arises with pass-by-value subclasses
- Kevin Bailey (30/30) May 15 2022 Hi Mike (and Guillaume, since you posted the same link),
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (17/20) May 15 2022 Yes, slicing is not the issue. Slicing is a problem if you do
- Mike Parker (20/38) May 15 2022 Pass struct instances by ref or by value as needed, just as you
- =?UTF-8?Q?Ali_=c3=87ehreli?= (82/99) May 15 2022 Hi from an ex-C++'er. :) I managed to become at least a junior expert in...
- Tejas (5/7) May 15 2022 Never say never :
- =?UTF-8?Q?Ali_=c3=87ehreli?= (8/11) May 16 2022 I see: It indeed appears on some pages on dlang.org but the language
- forkit (3/9) May 18 2022 Here is a very interesting article that researches this subject.
- Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= (13/14) May 18 2022 Yeah, he got it right. It is syntax sugar that makes verbose C++
- H. S. Teoh (16/18) May 15 2022 [...]
- IGotD- (7/9) May 16 2022 Common practice is that a class has class members itself. So
- H. S. Teoh (27/40) May 16 2022 [...]
- =?UTF-8?Q?Ali_=c3=87ehreli?= (6/7) May 15 2022 I think a more fundamental question is why structs and classes both
- Johan (9/15) May 16 2022 What is very problematic is that you cannot see the difference in
- Alain De Vos (4/4) May 16 2022 A new syntax like "*" should introduce something new.
- Kevin Bailey (17/21) May 16 2022 Hi Alain!
- =?UTF-8?Q?Ali_=c3=87ehreli?= (5/10) May 16 2022 I think we need a comparable D project to know whether this is really an...
- =?UTF-8?Q?Ali_=c3=87ehreli?= (11/18) May 16 2022 I see. Is it really a high mental load without the syntax? I seems to
- Johan (16/30) May 17 2022 `Foo a = b;`
- forkit (7/18) May 15 2022 A virtual function call has to pass through a virtual function
- Walter Bright (27/28) May 15 2022 Great question.
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
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
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
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 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
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
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
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.Yes, that's where we differ. :) You see problems that can sneak up, I see problems disappeared.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.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).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 usefulSuch 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
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
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
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
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
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
On 5/15/22 13:05, Kevin Bailey wrote:I've been programming in C++ full time for 32 yearsHi 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
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
On 5/15/22 21:20, Tejas wrote:Never say never : https://github.com/dlang/DIPs/blob/master/DIPs/DIP1040.mdThanks! I am reading it now.Also, there's `opPostMove` already within the languageI 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
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
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
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
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:[...] 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.`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 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
On Sunday, 15 May 2022 at 16:36:05 UTC, Ali Çehreli wrote:On 5/15/22 08:26, Kevin Bailey 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. 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. -Johanstructs 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.
May 16 2022
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
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
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
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
On Monday, 16 May 2022 at 21:20:43 UTC, Ali Çehreli wrote:On 5/16/22 10:35, Johan wrote:`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)` ? -JohanWhat is very problematic is that you cannot see thedifference insyntax. In my opinion it would have been much better if thelanguagerequired 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.
May 17 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 15 2022