Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

That is only a small fraction of RAII. The trivial fraction. If malloc fails, real RAII gives you an exception, which you may catch somewhere convenient. Real RAII runs a constructor. If construction fails, you get an exception. Lacking those, it all has to be done and checked by hand on the spot, and is often wrong, because not tested.


Not sure that's the part of C++ I'd heap praise on. For one thing, real RAII invokes the destructor, not a constructor. If your constructor throws, the destructor isn't going to be called. If your destructor throws, you're one passing exception away from std::terminate. Plus malloc doesn't throw, though new sometimes does.

Or, construction could return an optional/maybe style thing that you branch on, and then you don't need goto (sorry, non-local come-from with pretty branding) in your base language.


The important part of RAII is that the constructor actually acquires the resource.

The fact the OP insisted the important part of RAII is not that but running destructors shows he hasn't understood the most important concept in C++ programming.


The destructor is the distinguishing feature of RAII. Lots of languages have constructors without RAII because they do not have deterministic destructors. Examples are Python and Java.

Can you expound on what your understanding is?


The destructor is what distinguishes C++ from everything before and all but Rust even after, but is, as already explained above, just one part.

No exceptions, no RAII.


The downside of C++ is that exceptions can come from any possible function call depth, whereas in C you only have to deal with error return codes. So in that sense it’s easier to deal with errors in C, and harder to test the error paths in C++


That is the upside. You don't need to know how far down it happened. If you need to know more, you catch it lower. But most often you don't. Whatever the hell it was didn't work, and you do whatever is called for, then.


Exactly. Being forced to handle errors at every function call sounds great, but then you end being forced to bubble up errors you don't care about like Go does:

    res, err := func()
    if err != nil:
        return nil, err
That's not actually handling the error, and exceptions save you from being forced to pretend like you are. Just let it bubble up into the catch block further up the stack.


You don't have to code like that in Go, C or other languages without exceptions or fancy option-type sugar.

A sensible thing that often works great is to have a sticky error state (either a single int, or list of stuff you append to) then you just keep calling functions which will append to and/or replace the current error state until you reach a point where you can/care about handling errors, then you examine the persistent state and do something about it.


I hope you are not referring to things like errno? Because it, just like a returned error, must be checked after every call that might set it, before you know if you can safely proceed with the next.

Otherwise, what happens when you keep calling functions and there already is an error present? Are all functions implemented with if(errno) return null guards at the top? That's putting a lot of trust into global state and library writers. How do you know which functions become noops and which continue working in state of error?

Additionally, that would be a debugging nightmare, because if you keep calling functions before examining the error, how do you know which call introduced the error first?


> how do you know which call introduced the error first

You don't. You can't. You don't want to.

Take read()/write() for example. If you look into the kernel, it's physically impossible to name a function call that "introduced" the error. If you do a write() that will simply copy some memory into the buffer, and the syscall returns. When pages are flushed out to storage later, an error might be reported from storage asynchronously. The error is bubbling back at some point, at some syscall related to the file, but the error has nothing to do with that syscall necessarily. The error you get back could even be "caused" by a write to the same file but from a different process.

So it's perfectly reasonable that the FILE API, which wraps read()/write(), simply stores returned errors in the FILE Handle. Distributed systems are a perfect application for objects that do error isolation.


Delayed error ack is a completely orthogonal issue. Only necessary as a performance workaround, both in the case of unflushed disk buffers, sockets and distributed systems.

Parent presented sticky errors as an effective substitute for exceptions or error codes. Delayed errors is not a way to organize error handling easier, which I believe this topic was about. Delaying the error ack has quite the contrary effect, fail fast whenever possible will always be more accurate. How that surfaces to the caller is the more relevant question.

What happens when the disk is full or disconnected when copying 4GB src file to dst file after 100MB progress? (Yes, this error might occur slightly delayed due to buffers.) You surely don’t want to continue reading the remaining 3.9GB source file and call write() in noop-mode another thousand times in your loop before realizing this error on flush. Adding manual flushing just to check the error both negates the performance from the buffer and introduces extra complexity for a simple error check. Hence, every individual write must be checked regardless.

Such buffers are not infinite either. What do you do when write() fails because the buffer is full (EAGAIN)? Again back to square one of checking each individual write call instead of only checking the final error of flush or close.


If you look around there are lots and lots of objects that are "distributed", or aren't but should be. Synchronicity is often what's killing performance and introducing complexity.

> You surely don’t want to continue reading the remaining 3.9GB source file and call write() in noop-mode another thousand times in your loop before realizing this error on flush

It can be completely reasonable to back out only at strategic points. Copying a few KB or MB of memory more will rarely matter for an error case that shouldn't be optimized for. If there is an error, you'll typically want to reset a larger context object anyway. It depends on the situation, but by not having to handle the error at first notice, you can sometimes simplify the logic.

> What do you do when write() fails because the buffer is full (EAGAIN)?

EAGAIN is a different beast, it's not a "real" I/O error. With better APIs you retrieve buffers first (often in a different phase), removing this class of errors completely. But you can mostly just ignore EAGAIN anyway. It's a transient error (or not an error at all, really) that simply tells you the reason why zero bytes were written.

With fwrite(), not sure if it is well specified how it should interact with non-blocking FDs and EAGAIN. Probably it doesn't even allow you to distinguish between EAGAIN and I/O errors. It could also be an option to return a short write in this case (but I believe fwrite() needs to set either the error of EOF flag if it returns a short write). I also think fwrite() is largely not used with non-blocking FDs.


Yes like errno, except not global of course, you add one per struct/context/module/thread/whatever. And you design the functions to early return if the error state is set.

>how do you know which call introduced the error first?

You rarely care about that but if you do you either make the error state stick to the first error, or as I said, make it a list you can append multiple errors to.


"Sticky states" work too; IIRC, it's how FPU exceptions on the x87 work. But I was responding to the upthread comment that "exceptions can come from any possible function call depth, whereas in C you only have to deal with error return codes." Sticky states still have that "issue." After all, they're just an alternative to try{}catch{} blocks.


Or Google dialect.

Rust, anyway, captures this foolishness in a macro, which saves lines of source code, but still costs cache footprint, branch predictor slots, and runtime.


The Rust try! macro, which I assume is what you're thinking of, has been obsolete for many years - you can still refer to it because the Rust compatibility promise is taken more seriously than in C++ but you will need to use rather awkward syntax to get at it in modern Rust editions since the keyword "try" is reserved since 2018 edition.

These days you'd use the Try operator ? which is not a macro, it's an operator.

Try is really interesting, it's a unary operator so it takes a single parameter and it maps that parameter into a control flow decision. For Result the effect is similar to what you got out of the try! macro, but of course this operator can be implemented on any type. The standard library provides six implementations, including famously on Option, but also on ControlFlow itself, which is pretty nice.

This means across a complex system you can choose to collect Results, and decide what to do about the Results later, (perhaps after you have all of them, or after you've a certain amount) or you can choose the same for the ControlFlow decisions resulting from those Results.

You can also turn things on their heads, and decide that what you want to do is return early on success, but continue processing for errors -- which is something that's just unthinkable in an exception world where control flow and success are somehow the same thing. Rust took some years to figure out that's just not true which would be embarrassing if the assumption that it's true wasn't baked into the entire C++ language.


You still wouldn't want to throw for the success case, so the point is meaningless. And of course you can return early on success in C++, and continue to process details further otherwise, with zero difficulty, so there is no cause for embarrassment.

Rust people should be embarrassed if they carved out something special for this niche case.


> And of course you can return early on success in C++, and continue to process details further otherwise, with zero difficulty

Alas, C++ exceptions don't permit this, on failure the exception will get thrown and control flow switches away without any opportunity to intervene. That is in fact its whole purpose. That's the design mistake, it's not something you can fix, it's a choice which seemed clever last century, and it's last century's design.


It is in fact absolutely trivial to do, if you care to. But of course it is vanishingly rare to want it, so you don't see it much.

If you cannot figure it out, find someone to explain it to you, instead doubling down when your falsehood is pointed out.


I'm guessing you're thinking of capturing and hauling around exception_ptrs ? But that's far from "trivial" - it involves needing to understand how the temporary objects which were constructed in some "unspecified storage" work and then wrangle this custom smart pointer. In contrast Rust's Result and ControlFlow are just ordinary sum types, no magic involved.


  std::optional<T> f() {
    try { return it(); }
    catch(...) { 
      clean_up();
      return {}; 
    }
  }
Trivial. If you don't want to throw away the exception:

  T f() {
    try { return it(); }
    catch(...) { 
      clean_up();
      throw; 
    }
  }
But of course cleanup code belongs in a destructor, so nobody does that. In C++, we say

  T f() { return it(); }
and the responsible destructor does its job, either way.


This seems like you didn't even grasp what the problem was, let alone try to solve it.


I solved the right problem, instead. Your "sum type" is a workaround for lack of a fundamental language feature. We don't need that workaround.


There are two sum types here, and the fact they're different is the insight.

The try! macro didn't have that insight, and the first attempt to make a Try operator didn't either, but the current one does. This sort of experimentation is not available in practice to C++ but that would only slow it down a little, what prevents forward motion far more is that WG21 doesn't want to learn from other people's experiments.

C++ 23 gets std::expected which is, modulo IFNDR nonsense, a Result type. But C++ 23 doesn't have, and none of the further papers propose, a type analogous to ControlFlow.

I actually wondered how C++ 23 does the equivalent of Iterator::try_fold without ControlFlow, how do they express this idea? Did they use std::expected here as once Rust used Result ? The answer seems to just be "They don't" which I think gets to the heart of it.

A resumable, short-circuiting fold would be just as useful in C++ as it is in Rust, but it's easy to express nicely in Rust and doing so in C++ would insight anger from Exceptions purists, so that likely won't happen.


Don't forget to check for EINTR and keep a list of resources across the call stack in case a longjmp() is called.


Ideally I think there is a time and a place for both.

If you can't test it you've probably got that balance wrong.

That being said this realization is basically a post-functional thing, so a lot of libraries are stuck with one or the other.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: