Hacker News new | ask | show | jobs
by lionkor 680 days ago
No, noexcept confusingly doesn't mean "does not throw exceptions" in that sense. There is no constraint that says you can only call noexcept code from noexcept code - quite the opposite. Noexcept puts NO constraints on the code.

All noexcept does is catch any exception and immediately std::terminate. Confusingly this means that noexcept should really be called deathexcept, since any exception thrown within kills the program.

3 comments

> All noexcept does is catch any exception and immediately std::terminate.

While that's a possible implementation; the standard is a bit more relaxed: `noexcept` may also call `std::terminate` immediately when an exception is thrown, without calling destructors in the usual way a catch block would do.

https://godbolt.org/z/YTe84M5vq test1 has a ~S() destructor call if maybe_throw() throws; test2 never calls ~S().

MSVC does not appear to support this optimization, so using `noexcept` with MSVC involves overhead similar to the catch-block.

More than an optimization is a different exception handling philosophy.

AFAIK itanium ABI exception handling requires two phase unwinding: first the stack is traversed looking for a valid landing pad: if it succeeds then the stack is traversed again calling all destructors. If it fails it calls std terminate. This is actually slower as it need to traverse twice, but the big advantage is that if the program would abort, the state of the program is preserved in the core file. This is easily generalized with noexcept functions: no unwind info is generated for those, so unwind always fail.

MSVC used to do one pass unwind, but I thought they changed it when they implemented table based unwind for x64.

> All noexcept does is catch any exception and immediately std::terminate.

I don't think this is a decent interpretation of what no except does. It misses the whole point of this feature, and confuses a failsafe with the point of using it.

The whole point of noexcept is to tell the compiler that the function does not throw exceptions. This allows the compiler to apply optimizations, such as not needing to track down the necessary info to unwind the call stack when an exception is thrown. Some containers are also designed to only invoke move constructors if they are noexcept and otherwise will copy values around.

As the compiler omits the info required to recover from exceptions, if one is indeed thrown and bubbles up to the noexcept function then it's not possible to do the necessary janitorial work. Therefore, std::terminate is called instead.

Except the compiler still needs to ensure that if an exception bubbles through a noexcept function, std::terminate will be called, even if the exception were caught at some level above. So it still needs to keep track of the noexcept frames, even when inlining the noexcept function.

Generally what a language feature is intended to do is less relevant than what it actually does, over time. Just like the "inline" keyword was intended as a compiler hint, but what it actually does is change the visibility of symbols across compilation units, and alter the rules for static variables.

Of course, noexcept isn't as useless as inline, yet. There are real uses of it as a hint in template metaprogramming, as you said.

> Except the compiler still needs to ensure that if an exception bubbles through a noexcept function, std::terminate will be called, even if the exception were caught at some level above.

That's the failsafe I mentioned.

> Generally what a language feature is intended to do is less relevant than what it actually does, over time.

Not true, and again this take misses the whole point. The point of noexcept is to declare that a function does not throw exceptions. If a function is declared as noexcept, the whole point is that it does not throw exceptions. How the runtime handles a violation of this contract is something that only expresses in a bug. The same applies for exceptions thrown in destructors. It makes no sense to describe destructors as something that calls std::terminate .

How do you feel about the inline keyword?

Also, of course destructors are not "something that calls std::terminate", because they do many other things.

For function definitions, noexcept is perfectly equivalent to wrapping the body of your function in try {} catch (...) {std::terminate();}. In fact, noexcept doesn't even add any extra optimization options for the compiler: the compiler can already use the presence of such a block to decide that your function can't throw exceptions and optimize accordingly. Whether it can also ellide this block is a different matter, and noexcept doesn't help with that. It's just a nifty piece of syntax instead of this block.

Conversely, the noexcept declaration on the function doesn't allow the compiler to deduce anything about exceptions not being thrown inside the function, because the standard doesn't forbid throwing exceptions.

The only purpose noexcept serves other than a try/catch block is when used with declarations - there is indeed a difference in how the compiler can optimize a call to a function declared `extern void foo() noexcept;` and a function declared as `extern void foo();`

Oh, so it's like Rust's panic=abort
Panic=abort is more like -fno-exceptions since it applies to all the code being compiled and not just function. Codegen can also take advantage of the fact that it won't have to unwind.

I don't think there is a rust equivalent of noexcept.

Does C++ std::terminate unwind the stack and call destructors? Then noexcept would be pretty close to regular Rust panics, wouldn't it?

Basically an unrecoverable exception?

The behavior of `-fno-exceptions` isn't standardized, because it's a compiler feature, not a part of the C++ standard. The standard says:

> In the situation where no matching [exception] handler is found, it is implementation-defined whether or not the stack is unwound before std::terminate is invoked. In the situation where the search for a handler encounters the outermost block of a function with a non-throwing exception specification, it is implementation-defined whether the stack is unwound, unwound partially, or not unwound at all before the function std::terminate is invoked.

So, the whole thing is basically implementation-defined (including `-fno-exceptions`, since that is something that implementing compilers provide).

> Does C++ std::terminate unwind the stack and call destructors?

No, it doesn't. Try this: https://godbolt.org/z/cvG17cYEs