C++ Stockholm Syndrome


  • Impossible Mission - B

    @gąska said in C++ Stockholm Syndrome:

    Try zero-filling SQL connection, or file handle, or callback, or graphics context, or mutex, and see how it goes. And no, null object which you can't use before further initialization isn't valid object.

    It's a valid null object.

    Besides, if you're about to initialize something with real values, why initialize it with zeros? Wouldn't it... decrease performance?

    No, block-filling with 0s is faster than running a bunch of default constructors on data you're about to initialize with real values.


  • Banned

    @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Try zero-filling SQL connection, or file handle, or callback, or graphics context, or mutex, and see how it goes. And no, null object which you can't use before further initialization isn't valid object.

    It's a valid null object.

    Null object isn't valid. If it was, it wouldn't be null.

    Besides, if you're about to initialize something with real values, why initialize it with zeros? Wouldn't it... decrease performance?

    No, block-filling with 0s is faster than running a bunch of default constructors on data you're about to initialize with real values.

    If you write your constructor properly, you're only going to initialize your object once - in initialization list. Not using initialization list is bad and you should feel bad.



  • @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    If exception is thrown i constructor body, all members are destroyed.

    I mean, what else did you think happens? This is the only way it might possibly work.

    Yeah, that's the part I'm wondering about. Because it seems like the only way it might possibly work involves destructing all members, whether or not they've been constructed at all, which involves trying to treat uninitialized members containing random garbage as real data, leading to corruption, undefined behavior, and 🍄☁.

    What part of "they are initialized in declaration order" are you having problems with. It is known which items are constructed and which are not!



  • @thecpuwizard said in C++ Stockholm Syndrome:

    @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    If exception is thrown i constructor body, all members are destroyed.

    I mean, what else did you think happens? This is the only way it might possibly work.

    Yeah, that's the part I'm wondering about. Because it seems like the only way it might possibly work involves destructing all members, whether or not they've been constructed at all, which involves trying to treat uninitialized members containing random garbage as real data, leading to corruption, undefined behavior, and 🍄☁.

    What part of "they are initialized in declaration order" are you having problems with. It is known which items are constructed and which are not!

    Knowing what's initialized doesn't mean the compiler knows how to clean it up... PODs/classes? Sure. HANDLE? 🤷♂ That's what my dtor (which isn't going to be called) is used for.



  • @dcon said in C++ Stockholm Syndrome:

    @thecpuwizard said in C++ Stockholm Syndrome:

    @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    If exception is thrown i constructor body, all members are destroyed.

    I mean, what else did you think happens? This is the only way it might possibly work.

    Yeah, that's the part I'm wondering about. Because it seems like the only way it might possibly work involves destructing all members, whether or not they've been constructed at all, which involves trying to treat uninitialized members containing random garbage as real data, leading to corruption, undefined behavior, and 🍄☁.

    What part of "they are initialized in declaration order" are you having problems with. It is known which items are constructed and which are not!

    Knowing what's initialized doesn't mean the compiler knows how to clean it up... PODs/classes? Sure. HANDLE? 🤷♂ That's what my dtor (which isn't going to be called) is used for.

    That is why "HANDLE" (or anything that is effectively a pointer) does NOT be a member. std::auto_ptr or similar is what you should use.

    This is part of the changes in style I referred to earlier. 10-20 years ago usage of such problematic types were identified, and diligent developers have had years to root them out.


  • Impossible Mission - B

    @thecpuwizard Yeah, that's the point. You can take years and decades to "root them out" and figure out a bunch of hacks to make it work somehow... or you can design the language well in the first place.

    For example, in Delphi, when you call a constructor, it zeros out everything before the constructor body runs. If you throw an exception inside the constructor, it calls the destructor. In the destructor, you can call Free on each sub-object in your object, which will call that object's destructor if and only if it's non-null. This way, at any point in the construction process, your object is in a known state and can be destructed safely. That was intentionally designed that way, and it was only possible by coming up with a handful of concepts that C++ does not have.


  • ♿ (Parody)

    @masonwheeler said in C++ Stockholm Syndrome:

    Yeah, that's the part I'm wondering about.

    Not very hard. You are determined to not understand it lest your terrible opinion of it be changed.


  • Banned

    @masonwheeler said in C++ Stockholm Syndrome:

    @thecpuwizard Yeah, that's the point. You can take years and decades to "root them out" and figure out a bunch of hacks to make it work somehow... or you can design the language well in the first place.

    What difference at this point does it make?

    For example, in Delphi, when you call a constructor, it zeros out everything before the constructor body runs.

    So much for performance.

    If you throw an exception inside the constructor, it calls the destructor.

    So the destructor has to check every time whether it constructed successfully, partially or not at all - and if partially, which level of partially.

    In the destructor, you can call Free on each sub-object in your object, which will call that object's destructor if and only if it's non-null.

    Okay, so the good news is that the check is automated. The bad news is, you can't opt out. So much for performance.

    This way, at any point in the construction process, your object is in a known state and can be destructed safely. That was intentionally designed that way, and it was only possible by coming up with a handful of concepts that C++ does not have.

    And only necessary because of the handful of C++ concepts that Delphi doesn't have.

    Okay, enough trolling - let's get serious for a moment. I don't know what happened between you and C++. Maybe it stole your wallet. Maybe it dumped your sister. Whatever, everyone has their own reasons to hate C++. But your arguments are beyond ridiculous now. Of all the things C++ has done wrong, you've picked about the only thing it has done absolutely right. RAII is the best pattern for cleaning up resources (of any kind) and C++ implementation of it both guarantees 100% correctness in every situation imaginable and provides maximum performance. It could be improved a little - for example, not enforce initialization in order of declaration - but there's nothing really wrong about its model. If you have RAII everywhere, it works flawlessly.


  • Impossible Mission - B

    @gąska said in C++ Stockholm Syndrome:

    Of all the things C++ has done wrong, you've picked about the only thing it has done absolutely right. RAII is the best pattern for cleaning up resources (of any kind) and C++ implementation of it both guarantees 100% correctness in every situation imaginable and provides maximum performance. It could be improved a little - for example, not enforce initialization in order of declaration - but there's nothing really wrong about its model. If you have RAII everywhere, it works flawlessly.

    No.

    Good features get copied. If RAII is so "flawless," why does nothing but C++ implement it? Why don't we see it everywhere?



  • @gąska said in C++ Stockholm Syndrome:

    If you have RAII everywhere, it works flawlessly.

    Any feature that requires a human being to implement it without error a thousand times is not a good feature.

    It might be "good" by the standards of C++, I suppose.


  • Banned

    @blakeyrat said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    If you have RAII everywhere, it works flawlessly.

    Any feature that requires a human being to implement it without error a thousand times is not a good feature.

    You can't get it wrong unless you do some really stupid shit (or write C).


  • BINNED

    @gąska said in C++ Stockholm Syndrome:

    Most exceptions can be replaced with error returning quite easily. Constructor exceptions are about the only ones that can't.

    The whole point of exceptions was to get rid of the "every function returns an error value" style, or even worse, the errno style.


  • Banned

    @masonwheeler said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Of all the things C++ has done wrong, you've picked about the only thing it has done absolutely right. RAII is the best pattern for cleaning up resources (of any kind) and C++ implementation of it both guarantees 100% correctness in every situation imaginable and provides maximum performance. It could be improved a little - for example, not enforce initialization in order of declaration - but there's nothing really wrong about its model. If you have RAII everywhere, it works flawlessly.

    No.

    Good features get copied.

    Hahahahaha xD You couldn't say anything more wrong. Familiar features get copied. It took decades for lambdas to be copied from LISP to mainstream languages. C++ iterators are designed to work like pointers, which is the most stupid thing to do, but it was familiar. Everyone copied constructors from C++ even though they were redundant with factory methods.

    If RAII is so "flawless," why does nothing but C++ implement it?

    Also, Rust. Also, Nim. Also, a bunch of other niche languages.

    Why don't we see it everywhere?

    Because the world is chaotic. Stupid features get promoted as much as good ones. Every industry has some stuff that could be better but isn't for historical reasons. NTSC. Feet and inches. Left-hand traffic. A dime smaller than a nickel. Green traffic light. The first 30 characters of ASCII. W. 16:9. Implicit conversions.


  • ♿ (Parody)

    @blakeyrat said in C++ Stockholm Syndrome:

    Any feature that requires a human being to implement it without error a thousand times is not a good feature.

    Yeah, that's what I love about RAII. You only have to get stuff right in one place (an object's destructor) instead of getting it right every time you use each object.


  • BINNED

    @gąska said in C++ Stockholm Syndrome:

    @masonwheeler before constructor body is run, members are initialized in the order they were declared in class body, with constructor arguments taken from initialization list. If an exception is thrown in member constructor, all previous members are destroyed. If exception is thrown i constructor body, all members are destroyed.

    I mean, what else did you think happens? This is the only way it might possibly work.

    I want to reiterate that. The way it works is the only sensible way to do it, destructing fully constructed members but not calling destructors on something which isn't fully constructed.

    Also, regarding this:

    @blakeyrat said in C++ Stockholm Syndrome:

    If you have RAII everywhere, it works flawlessly.

    Any feature that requires a human being to implement it without error a thousand times is not a good feature.

    You only write an RAII class once per type of resource. Which means, most likely your library has already written it, like std::shared_ptr or std::fstream or whatever.
    Most of the time you can follow the "rule of zero" and not write a destructor at all, since the default constructor does exactly what you need: destruct members.



  • @topspin Well that's good. I only used C++ during the dark years, when I foolishly thought converting a program from C to C++ would actually provide a benefit. Someday I should actually make the effort to use modern C++.

    Except I'd still hate it so no.


  • BINNED

    @gąska said in C++ Stockholm Syndrome:

    @topspin said in C++ Stockholm Syndrome:

    How does Rust (to use that example) deal with things like copy construction? Is all that explicit?

    Yep. The standard library has the Clone trait with clone() method that you can #[derive] (auto-implement) on your types. You can also implement it manually where needed. You can also choose not to implement it (it's the default).

    What happens when I pass-by-value a type that doesn't have the clone trait? Or, more to the point where I don't understand the mechanism:

    • What happens when I pass-by-value an object with the Clone trait without calling clone; and likewise
    • how does a clone()d object outside the function call then get passed into the function?

    Honest question, just curious.


  • BINNED

    @blakeyrat It is much better than you remember, but I don't see how your assessment of "C# is far superior" is wrong for your use case. So yes, you'd have reason to hate it.


  • Banned

    @topspin said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Most exceptions can be replaced with error returning quite easily. Constructor exceptions are about the only ones that can't.

    The whole point of exceptions was to get rid of the "every function returns an error value" style

    Why?



  • @gąska said in C++ Stockholm Syndrome:

    @topspin said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Most exceptions can be replaced with error returning quite easily. Constructor exceptions are about the only ones that can't.

    The whole point of exceptions was to get rid of the "every function returns an error value" style

    Why?

    (As I have asked previously) How would you [gaska] treat errors that occur at a low level being passed up to a higher level [skipping perhaps dozens of intermediate levels] without having to account for them in the intermediate levels if you do not use exceptions???


  • area_can

    @thecpuwizard longjmp 🚎


  • BINNED

    @gąska said in C++ Stockholm Syndrome:

    @topspin said in C++ Stockholm Syndrome:

    The whole point of exceptions was to get rid of the "every function returns an error value" style

    Why?

    I wasn't saying it's good, I only (up to here) said that was why they were introduced.

    Anyways, the idea is that you don't interleave all of your happy path with the error path everywhere, and you can still use your actual return type to return something useful. (I know that doesn't apply to modern stuff where you return e.g. an optional, but it does apply to the old style that gave rise to exceptions)

    HRESULT something(BAZ* bazOut)
    {
      FOO foo;
      HRESULT res = getFoo(&foo);
      if (!SUCCEEDED(res))
      {
        // ... clean up
        return res;
      }
    
      BAR bar;
      res = getBar(&bar);
      if (!SUCCEEDED(res))
      {
        // ... different clean up
        return res;
      }
    
      res = frob(&foo, &bar, bazOut);
      if (!SUCCEEDED(res))
      {
        // yada yada
        return res;
      }
    
      return ERROR_SUCCESS;
    }
    

    in contrast to: (yes, I've mixed in some other style changes, too)

    BAZ something()
    {
      FOO foo = getFoo();   // make FOO exception-safe and clean up behind itself
      BAR bar = getBar();
      return frob(foo, bar);
    }
    

    EDIT: Also, error codes can and often are ignored by the caller. This is again a weakness which modern variations do not have, I guess.



  • @bb36e said in C++ Stockholm Syndrome:

    @thecpuwizard longjmp 🚎

    I have a 🚎 for you... just stand here on the tracks for a few minutes... 👿


  • Banned

    @topspin said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    @topspin said in C++ Stockholm Syndrome:

    How does Rust (to use that example) deal with things like copy construction? Is all that explicit?

    Yep. The standard library has the Clone trait with clone() method that you can #[derive] (auto-implement) on your types. You can also implement it manually where needed. You can also choose not to implement it (it's the default).

    What happens when I pass-by-value a type that doesn't have the clone trait?

    Bytewise move. Currently Rust doesn't have "move constructors" or anything like that. So all moves are bytewise copies. Also, you can move anything anywhere as long as it's not borrowed.

    • What happens when I pass-by-value an object with the Clone trait without calling clone; and likewise

    Instead of calling clone(), it does bytewise move. No additional logic happens because no additional logic can be implemented.

    • how does a clone()d object outside the function call then get passed into the function?

    Depending on function signature, it will be either moved into or borrowed (passed by reference) to function. Really, it's all very simple and straightforward, much less complex than C++ move semantics, no std::move, no rules to remember.


  • Banned

    @thecpuwizard said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    @topspin said in C++ Stockholm Syndrome:

    @gąska said in C++ Stockholm Syndrome:

    Most exceptions can be replaced with error returning quite easily. Constructor exceptions are about the only ones that can't.

    The whole point of exceptions was to get rid of the "every function returns an error value" style

    Why?

    (As I have asked previously) How would you [gaska] treat errors that occur at a low level being passed up to a higher level [skipping perhaps dozens of intermediate levels] without having to account for them in the intermediate levels if you do not use exceptions???

    Rust made the design choice of making all errors loud and obnoxious, as to make sure programmers actually handle errors. AFAIK currently all you have to do is make some error type, derive some trait implementations, and then, whenever you don't feel like handling error, you just ? the value (postfix unary operator) and it automagically cobbles up to whoever actually wants to handle it.


  • BINNED

    @gąska I see. That makes sense.

    @gąska said in C++ Stockholm Syndrome:

    Really, it's all very simple and straightforward, much less complex than C++ move semantics, no std::move, no rules to remember.

    But it puts more burden on you to remember other things, like calling clone. If you forget to do that, you might have a bug. And you can't create types which are non-copyable. I think I prefer the constructor approach.

    Still, I'll need to check out rust some time when I finally get around to.


  • Banned

    @topspin said in C++ Stockholm Syndrome:

    @gąska I see. That makes sense.

    @gąska said in C++ Stockholm Syndrome:

    Really, it's all very simple and straightforward, much less complex than C++ move semantics, no std::move, no rules to remember.

    But it puts more burden on you to remember other things, like calling clone.

    Better than having to remember when to move, since if you get it wrong, instead of silently going forward and getting inferior performance, you get compile error due to failed borrow check.


  • BINNED

    @gąska said in C++ Stockholm Syndrome:

    you get compile error due to failed borrow check.

    If that's always the case, you definitely have a good argument.
    Or wait, maybe not, that seems orthogonal to having constructors or not. But it's cool anyway. 👍



  • @masonwheeler said in C++ Stockholm Syndrome:

    @thecpuwizard said in C++ Stockholm Syndrome:

    re: Constructors and Exceptions..... The reason for the "olde school" of NO, was because an exception in the constructor does not invoke the destructor.

    :wtf:

    Yet another example of how poorly thought-out the C++ language is.

    The sentence is misleading. The destructors of all the members get called, of course. The destructor of the object whose constructor threw can't be called because it was never constructed in the first place - in the destructor, you must have a completely valid object to work with, and since the constructor never finished, you'd have no idea what members were initialized or not. it would be incredibly dangerous to call the destructor in this case, as the members after the one that threw would be garbage memory.

    EDIT: I was ninja'd



  • @blakeyrat said in C++ Stockholm Syndrome:

    when I foolishly thought converting a program from C to C++ would actually provide a benefit

    Yeah, back in the dark years we didn't have zero-cost exceptions yet, but nowadays you can get improved performance over C-style error handling if errors aren't happening constantly in your application.

    I don't blame you for hating C++. We all do. I just really love some of its features enough to outweigh the ones I hate.



  • @gąska Yeah, you're right, I was misremembering the reason. 👍


  • Considered Harmful

    @benjamin-hall said in C++ Stockholm Syndrome:

    @jaloopa said in C++ Stockholm Syndrome:

    @lb_ Not having exceptions is a problem whether you have constructors or not. Most exceptions I throw aren't from constructors

    I thought it was best practice to make sure your constructors can't throw exceptions (usually by doing nothing but assignment and very simple exception-safe calculations). But what do I know, I never went to any of those fancy programming schools.

    RAII.

    if let Some(file) = File::open("foo.txt") {
        do_something_with(&file);
    }
    

  • Considered Harmful

    @topspin said in C++ Stockholm Syndrome:

    @gąska I see. That makes sense.

    @gąska said in C++ Stockholm Syndrome:

    Really, it's all very simple and straightforward, much less complex than C++ move semantics, no std::move, no rules to remember.

    But it puts more burden on you to remember other things, like calling clone. If you forget to do that, you might have a bug. And you can't create types which are non-copyable. I think I prefer the constructor approach.

    The compiler checks it for you. Types which are completely stack-allocated can have Copy auto-implemented, which means that passing by value does a bitwise copy; if you pass a type by value which isn't Copy, then it moves it and you're not allowed to use it afterwards. So most things take pointers instead of values.


  • Discourse touched me in a no-no place

    @topspin said in C++ Stockholm Syndrome:

    This is again a weakness which modern variations do not have

    You underestimate the sheer laziness of most programmers…


  • Discourse touched me in a no-no place

    @masonwheeler said in C++ Stockholm Syndrome:

    Why don't we see it everywhere?

    The features that seem to have been rather more widely copied are the C# using (I'm pretty sure that that wasn't the origin either, but I don't recall where it originally came from) and functions which take a callback block which they pass the lifetime-scoped object into. The latter is all over the place (common pattern in JS and Ruby), but I'm pretty sure that it cropped up in Lisp first.

    RAII isn't a bad pattern per se, but it does lead to needing the programmer to be able to intuit that declarations of variables in functions can have significant effects at other locations in the code. (It's particularly the way it looks with locks that I don't care for; the declared variable isn't used for the rest of the block and that just seems wrong to me, even though I know what is going on.)


  • Banned

    @dkf one of the things that Rust did better that C++ is design the standard library in a way that enforces correct usage of RAII resources at compile time. For example, if some variable is guarded by mutex, it's literally impossibe to (ie. there's no sytax that lets you) access it without acquiring the lock first (the lock doubles as an accessor object). Borrow checker ensures that you cannot free a resource before it stops being used by everyone. In result, there are no objects that only "exist" - there are actually being used. And forgetting about them is compile error.


  • Considered Harmful

    @gąska I have one thing I know for sure about Rust code. That thing is that after all my writing random shit in it, yesterday was the first time ever that I had a logic error that the compiler didn't catch.


  • Discourse touched me in a no-no place

    @pie_flavor said in C++ Stockholm Syndrome:

    yesterday was the first time ever that I had a logic error that the compiler didn't catch.

    There are always higher-order logic errors that compilers can't catch. Why? We want our compiles to terminate. :D The deep problem is that describing the full semantics of a program is hard, really really hard, so much so that we usually only want to do it once, and that's called the program itself. Without an independent description that says what the program should do, compilers (and other tools) can only check for the subset of rules that are encoded in, say, the type system, and type systems are usually deliberately restricted to being significantly less complex than the program itself in order to keep them tractable.

    General program correctness checks are isomorphic to mathematical theorem proving. They're the same damn (super-difficult!) problem in slightly different clothing. I think that's actually pretty neat, even if sometimes frustrating too…


  • ♿ (Parody)

    @dkf said in C++ Stockholm Syndrome:

    General program correctness checks are isomorphic to mathematical theorem proving.

    Except that the damned users keep changing the damn theorems out from under me!


  • Discourse touched me in a no-no place

    @boomzilla said in C++ Stockholm Syndrome:

    @dkf said in C++ Stockholm Syndrome:

    General program correctness checks are isomorphic to mathematical theorem proving.

    Except that the damned users keep changing the damn theorems out from under me!

    0_1523960134869_ee153872-e679-4fcb-bd37-65c286fb3227-image.png



  • @boomzilla said in C++ Stockholm Syndrome:

    @dkf said in C++ Stockholm Syndrome:

    General program correctness checks are isomorphic to mathematical theorem proving.

    Except that the damned users keep changing the damn theorems out from under me!

    Yeah, for me that's the product managers (or designers) changing the requirements out from under us.
    :phb:: We'll never change this assumption. We can't. The server relies on it.
    dcon: <codes to that assumption>
    :phb:: This assumption is wrong.
    dcon: :headdesk: again. And again.
    (current day)
    :phb:: We'll never change this assumption. We can't. The server relies on it.
    dcon: <codes assuming it will change>
    :phb:: Guess what?
    dcon: <nailed it>



  • @dcon said in C++ Stockholm Syndrome:

    @boomzilla said in C++ Stockholm Syndrome:

    @dkf said in C++ Stockholm Syndrome:

    General program correctness checks are isomorphic to mathematical theorem proving.

    Except that the damned users keep changing the damn theorems out from under me!

    Yeah, for me that's the product managers (or designers) changing the requirements out from under us.
    :phb:: We'll never change this assumption. We can't. The server relies on it.
    dcon: <codes to that assumption>
    :phb:: This assumption is wrong.
    dcon: :headdesk: again. And again.
    (current day)
    :phb:: We'll never change this assumption. We can't. The server relies on it.
    dcon: <codes assuming it will change>
    :phb:: Guess what?
    dcon: <nailed it>

    Remember the Agile Manifesto say "Welcome Changing Requirements"....however it does not say to welcome Missing or Wrong Requirements - those deserve a private session with the clue bat.



  • @gąska said in C++ Stockholm Syndrome:

    Blitting 0s usually doesn't give you valid object.

    You mean I can't call the functions from the vtable at 0x0000000000000000?


  • Discourse touched me in a no-no place

    @ben_lubar said in C++ Stockholm Syndrome:

    You mean I can't call the functions from the vtable at 0x0000000000000000?

    That depends… What architecture are you running on, and does it put the first page in user-writable memory?


  • BINNED

    @dkf Architectures that do are a security disaster.



  • @topspin said in C++ Stockholm Syndrome:

    @dkf Architectures that do are a security disaster.

    Also, if you need all the address space with 64-bit pointers, you're running some crazy expensive setup with like a billion RAM sticks.


  • Banned

    @ben_lubar people said the same about 32 bits in 2000.


  • BINNED

    @gąska I kind of doubt that.
    Edit: Well, maybe somebody said that, but it wasn't true.


  • Banned

    @topspin okay, 1995. Doesn't matter to me, both dates were before I learned what RAM is and that computers aren't all the same. The point is, any amount of memory will eventually get filled. It might take a while longert to fill 16 exabytes than it took 4 gigabytes, but it WILL happen, and our track record as an industry suggests a lot of programs running today will be still running then.


  • BINNED

    @gąska Still, while that works in spirit, Ben's "like a billion RAM sticks" is literally correct as of now. Unless you were running single digit Byte sized RAM sticks, that's wasn't true for 32 bits at any point. :pendant:


Log in to reply