← Back to context

Comment by forrestthewoods

1 day ago

Program state is significantly more complex than just needing some RAII resources to cleanup via destructors.

> during sections that may throw

Yeah one of the problems with exceptions is it’s impossible to know what “may throw” other than “well I guess literally anything so everything”. It is very irritating.

At the end of the day exceptions are just a little syntactic sugar. Or perhaps syntactic bitters.

It is notable that systems languages designed after C++ all chose to not include exceptions. Go, Zig, Swift, Odin, Jai.

Rust panics are kinda sorta exceptions in that they unwind. But their intended use case is for irrecoverable errors. And of course you can set panic=abort.

C++ exceptions are very rarely treated as so serious module level irrecoverability.

>Program state is significantly more complex than just needing some RAII resources to cleanup via destructors.

You're being rather vague. All throwing does is cause control flow to jump to the nearest catch that can handle the exception, destructing all objects along the way. I struggle to think of an example that could cause problems that isn't some variation of "I had some code after the exception that I needed to run, and it didn't run, because it wasn't set up to run at scope exit". I'd love to see such an example if you have one.

>it’s impossible to know what “may throw”

* If it's a throw statement, it may throw.

* If it's an expression that contains a 'new' operator, it may throw.

* If it's an expression that contains a dynamic_cast to a reference type, it may throw.

* If it calls a function that you don't know that it does not do any of the above, it may throw.

* If it's unknown if a function is called (e.g. types are templated), it may throw.

* Otherwise, it doesn't throw.

If you're managing resources manually, either make sure not to call any functions until you release them, or stop managing them manually. I encourage the latter.

  • > You're being rather vague.

    Completely forget about memory allocation and memory allocation like things.

    Let’s say I have a physics system that runs an update. Assume we catch outside the update. If anything throws the system is now in an intermediate state and is effectively irrecoverable.

    If you want to argue that exceptions should only be used for irrecoverable errors such that the subsystem is not expected to resume then I would list. This is akin to Rust panics which unwind like C++ exceptions but are not intended to resume. I have not read any comments in this large subthread arguing for using exceptions in this narrow style.

    > it’s impossible to know what “may throw”

    Yeah you’re just saying you should basically assume that any code can throw at any point. I think that’s a really really bad design pattern that makes code significantly harder to reason about. Control flow should be clear and obvious. “Any line of code could immediately unwind to unclear catch” is the objectively not clear and obvious.

    But let me turn it around. You tell me an example of a system that doesn’t use exceptions where adding exceptions makes it better.

    • >Let’s say I have a physics system that runs an update. Assume we catch outside the update. If anything throws the system is now in an intermediate state and is effectively irrecoverable.

      That's a transaction kind of scenario. You catch at a recoverable point and rollback to a good state, and if that's not possible, then you simply fail out. I don't understand; what problem did exceptions introduce here? An exception was thrown (i.e. an error happened) during an intermediate operation and the operation as a whole stopped. What would have changed if every function had used error codes instead? It would have been either an error you were incapable of handling (in the sense that you didn't even look at the error code), and the operation would have silently continued in a possibly corrupted state, or it would have been an error you were capable of handling, in which case implement that same handling logic for the exception code.

        if (op1() != SUCCESS){
          //recover and continue
        }
        if (op2() != SUCCESS){
          //nothing to do so just fail out
          return FAIL;
        }
        //don't care about error so don't even bother checking
        (void)op3();
        //now the state of the program may be invalid
        op4();
      

      becomes

        try{
          op1();
        }catch (/*...*/){
          //recover and continue
        }
        op2(); //let exception be caught by someone who can do something about it
        op3(); //same
        //sometimes we won't get here, but at least if we do, we know the state of the program is valid
        op4();
      

      Is it just that you like ifs more than try-catches?

      >If you want to argue that exceptions should only be used for irrecoverable errors such that the subsystem is not expected to resume then I would list.

      That would depend on what you mean by "irrecoverable error". I interpret that to mean that the program cannot continue to function safely and it's better to terminate ASAP than to attempt to do anything else at all. If that's what you mean then no, that's not what exceptions are for. Like I said in a sibling comment (https://news.ycombinator.com/item?id=48527216), exceptions are meant to signal an error that may or may not be recoverable that the immediate caller may not know how to handle. Someone in the call stack should be able to decide based on program state how to respond to the error condition; sometimes that will involve rolling back to a valid state, as I said before, perhaps retrying; sometimes you will cancel the operation and discard the interrupted computation; sometimes you will notify the user or log an error; sometimes you will decide that there's actually nothing else for the program to do and clean up and exit.

      >You tell me an example of a system that doesn’t use exceptions where adding exceptions makes it better.

      Sure, no problem:

        if (op1() != SUCCESS)
          return FAIL;
        if (op2() != SUCCESS)
          return FAIL;
        if (op3() != SUCCESS)
          return FAIL;
        //etc.
      

      with exceptions becomes

        op1();
        op2();
        op3();
        //etc.
      

      All else being equal, exceptions make this kind of code better by making it more readable. If the non-exception code needs to return both error codes and partial values, it becomes even more noticeable:

        auto [result1, error1] = op1();
        if (error1 != SUCCESS)
          return FAIL;
        auto [result2, error2] = op2(result1);
        if (error2 != SUCCESS)
          return FAIL;
        auto [result3, error3] = op3(result2);
      

      versus:

        op3(op2(op1()))

      7 replies →