Hacker News new | ask | show | jobs
by fivea 1637 days ago
> (...) such as Google's famous "no exceptions".

If I recall correctly, Google's rationale regarding exceptions is that their legacy code is not exception-safe, and so they were faced with the choice of either rewriting critical parts of their legacy code to handle exceptions, or don't use them.

Also, their "no exceptions" rule only applied to work involving their legacy code.

I'm too lazy to find the source, but that bit of trivia was already discussed ad nauseum even in HN.

The morale of the story is that you should not mindlessly repeat any opinion without knowing the rationale and instead pulling appeals to authority to cover up the logical hole. That's how you end up contradicting even your source, just because you believed that's how the cool kids do things.

4 comments

> Also, their "no exceptions" rule only applied to work involving their legacy code.

That's not the case, the no exceptions rule applies to legacy and non-legacy code: https://google.github.io/styleguide/cppguide.html#Exceptions.

(Opinions are my own)

> Also, their "no exceptions" rule only applied to work involving their legacy code.

The reasons for not using exceptions are outlined in this post: https://abseil.io/tips/76

absl::Status is basically exceptions without language sugar. Or, another way of looking at it, golang err in C++. Basically, non-local control flow is dangerous as it can't be evident to the programmer when something deep in the stack will bubble up an exception.

It is super annoying to deal with sometimes but there are a lot of amazing helper macros (yuck for other reasons) that exist. More details can be found looking through here: https://cs.opensource.google/search?q=ASSIGN_OR_RETURN&sq=

> Basically, non-local control flow is dangerous (...)

This is the crux of the error you're making. Exceptions are not about control flow. Exceptions are a transparent and clean way to handle exceptional events. Exceptions are not intended to, say, handle the status code of a HTTP requests. Exceptions are intended to handle exceptional and potentially unrecoverable errors in a safe and controlled manner, such as failing to allocate memory, regardless of where and how they pop up.

Therefore, suggesting classical C-style return codes or specialized monadic types to handle results as alternatives to exceptions completely misses the whole point of exceptions and, more importantly, all the classes of problems they are designed to eliminate.

> Exceptions are a transparent and clean way to handle exceptional events.

I'd sum up this argument as: Exceptions are syntax sugar that allow callers of a function to ignore the error cases of something they are calling and depend on something that calls into them to handle the error case.

An example:

    void HandleRequest(const Request &request, KV &kv) {
        ...
        kv.set("a", 10);
        ...
    }
In this example KV.set will raise an exception. The programmer implementing HandleRequest isn't directly exposed to this fact and so to them, and the reviewer, and future onlookers this code looks correct. Now lets say kv.set() throws an exception in production under a specific case. Maybe there were two people attempting to set the same value at the same time or a networking issue. Doing this in the context of a webserver might make sense as the webserver might handle exceptions as error codes but that's not the end-all-be-all. Suppose we refactored to something like this

    Something CreateSomething(....);
How do you know if this function will throw an exception? How do you find all of the possible exceptions that can be thrown? Statically you can't really. If instead you see

    absl::StatusOr<Something> CreateSomething(...);

You can tell for sure that the result has some error that needs to be handled. Your original code:

    kv.set("a", 10);
This code no longer compiles in an absl::Status world. Instead you'd need to do something like:

    CHECK_OK(kv.set("a", 10)) // this will panic
    RETURN_IF_ERROR(kv.set("a", 10)) // Bubble up

From this we get:

1. a stack trace since RETURN_IF_ERROR adds metadata about the call site.

2. Guarantee that code will not compile if errors are not handled.

3. Guarantee that future readers know that some very high level function could probably call into code that can produce an error you need to handle.

This matters much more if things like this are happening:

    kv.beginTransaction();
    kv.set(...);
    kv.endTransaction();

This could be handled by destructors in this case but in other cases:

    otherService.startingWork();
    kv.set(...);
    otherService.doNextStep();
There are cases where destructors do not make sense. You do not always want to call `doNextStep()` as it would be 100% wrong in the case where we cannot set our value in our kv store. Contrived but I've run into these in real life services. If a developer sends me the above code snippet I might LGTM. If the developer instead sends me:

    otherService.startingWork();
    if (!kv.set(...).ok()) { log("something went wrong"); }
    otherService.doNextStep();
I'll be able to point to the exact problem with this code much more easily. Also if there's an outage and I need to read this code I can clearly see why this `something went wrong` in the logs correlates to incorrectly called doNextStep().

I'm not saying that Status is perfect (I'm not 100% sold) but exceptions are a type of control flow in an abstract sense. The problem some people have with it is it's control flow you can't audit.

No.

Exceptions are a way to ensure that failures are directed immediately to a place designated to deal with such a failure.

They are, in particular, not any sort of "syntax sugar", unlike "?" in certain other languages, or your StatusOr thing. A function that throws an exception does not, in any sense, return to its caller. It does not construct any sort of return value. It does not consult the stack to see where it came from and resume running there.

And, exceptions are totally auditable. There is never any hint of ambiguity or uncertainty about where an exception will take you.

You can be confident that if a function was thrown from, it was because it could not perform the requested action. And, you can be confident that if an exception was not thrown the function called satisfied whatever postconditions it promised.

So, you don't need to know if a function might throw. You may instead assume any function might throw. If it doesn't, then it has satisfied its documented postconditions. Your obligation is only to ensure that destructors clean up any intermediate state on the way out. These identical destructors get exercised every time through the code, so are exercised frequently.

Error-handling code at places where the error cannot actually be dealt with properly, that just tries to propagate the failure up the call chain, is typically not well tested, and often cannot even be triggered in testing.

I guess we can consider AOSP legacy code then, given how they use C++.
Yes. Legacy in every sense, including "abandonware".
Educate yourself on Android source code.

You can even start by the new entries related to Rust.

https://source.android.com/setup/build/rust/building-rust-mo...

Why would I care about anything on that page?

I have no need to read hype about Rust, regardless of where it might run. And, I have no desire to build Android apps, in any language.

To educate yourself about what ISO C++ companies are actually doing with the language.

The reference to Rust was just an example on how such members are also looking for alternatives, other big names are looking into Swift, C#, whatever.

Meanwhile clang crawls along on its support for newer ISO C++ features, as the biggest contributors now have their focus elsewhere.

> Meanwhile clang crawls along on its support for newer ISO C++ features, as the biggest contributors now have their focus elsewhere.

What did you mean by this? I am not an LLVM expert or anything, but I find more-often-than-not most of the new features I read about that make its way into LLVM are requirements needed due to clang adding things from new C++ standards. These improvements then benefit other frontends like the one for Rust.

Indeed the details matter, but my example was just designed to be something that might be somewhat more recognizable than those situations and restrictions I've encountered personally.
> (...) my example was just designed to be something that might be somewhat more recognizable (...)

If your goal was to use that poor example to support the idea that people stick with subsets of C++ because of reasons, that example failed to support the assertion. Thus it makes no sense to stick with a patently wrong observation just because it's easier to recall.

Getting back to the topic, as far as I know there are only two features of C++ which are up for debate regarding their adoption: exceptions, and template metaprogramming. The exception-handling debate only makes sense in very low-level applications and refactoring legacy exception-less code, which in practice is not anyone's case. The template metaprogramming debate typically boils down to YAGNI and the need to avoid resume-driven development. Nevertheless, both features are used extensively, whether directly or indirectly (see STL), and in general there is no reason to bother debating whether people should use it or not, unless you have very specific requirements in mind (I.e., avoid generating magical code in embedded applications, or in very high performance applications where you feel you need tight control over everything down to which instructions are generated).