Only compiler bugs I've seen in C++ have all been around exceptions. I think this is just another reason towards force-handled error codes. You don't have a whole new control flow that needs to be verified and is much simpler from an implementation/runtime perspective.
Not really. See Rust/Scala/Haskell's Result/Either type, which has compiler support, but that compiler support is lightweight - you use the same control structures and flows as with regular code.
That's a great approach, but needs sum types and HKT to be effective, neither of which C++ really has (std::variant exists but doesn't always behave as a proper sum type AIUI) at present.
Rust manages it without HKT quite reasonably (rather than implementing generic monads, it has the `?` operator, which is syntax-sugar for something like [0]), though sum types are a necessity.
Absolutely - there's currently discussion around a Carrier trait which would make it generic, once we have some variety of HKT. Still, it's good enough for now, and any future development of it will be backwards-compatible.
Nope, if you have an operation that can fail then wrap it in a type that represents that(I'm partial to Rust's Result<T,E>) then you force consumers to deal with potential errors and you can do 1-call construct + init if you want to.
Exceptions cause all sorts of problems(like violating the principal of only paying for what you use). They aren't checked so you can't enforce handling via type system and in general they're just a mess in C++.
> Exceptions cause all sorts of problems(like violating the principal of only paying for what you use).
Exceptions are zero-cost in terms of performance on any sane architecture (this excludes x86, but not x64). The only cost that you're paying is the extra memory for the tables, which are all statically allocated.
Although I still don't see why, in your example, one of the variables cannot be elided even if goo does throw. Since there's no point at which address of r is taken, much less escapes the scope, it should always be safe to elide r and just return s, no?
They're zero(ish) cost at runtime, but focusing on just that is missing something: pervasive unchecked exceptions inhibit optimisations, both in compilers and in libraries, which have to be more defensive to preserve correctness.
If you replace exceptions with e.g. return values, it doesn't really change anything for the libraries - if they expect to be able to deal with an error, they still have to defensively preserve correctness.
OTOH the library always has the option to say "this particular thing that you give me should be nothrow; if it throws, that's U.B.", and not even try being defensive in the face of exceptions when they come out of such a thing.
I think you missed the precise qualification of my comment: this only applies when the compiler can't tell what exceptions can occur, like in C++, Rust and even Java, and so needs to be able to handle any/all of them. The most basic (and most relevant) query is "can this call throw?", but even that is impossible to answer without global information when there are unchecked exceptions.
In statically typed languages, return values, like (fully) checked exceptions, are different, as they're exactly the sort of global information the compiler needs. Functions can statically guarantee the exact set of ways in which it can interact with its caller, and the compiler can understand. This means a function that, say, sorts a list with a less-than function (that returns bool) knows that every time that function exits, it gives a value of true or false, no exceptions (hah).
In any case, just writing a library that says "UB to throw" isn't quite enough, as the compiler won't understand, and will still structure code to handle exceptions "sensibly", even thought it doesn't need to. Fixing that means being able to communicate nothrow to the compiler (which is nice too), but this starts to no longer be a pervasive unchecked error system: it's somewhat of a binary form of checking.
Well, we were talking about exceptions in C++, where there is a standard way to communicate nothrow. So yes, you're right that it's no longer a pervasively unchecked error system - but that's precisely why they're not any worse than Result<T,E> at this point. Effectively you can treat C++ as having every function return Result<T,E> by default, with nothrow being an opt-out to just return T.
(Explanation of the /s: panics use the same mechanism, so the compiler and libraries unfortunately still have to be just as defensive when they're enabled. When the compiler is in -C panic=abort mode, it doesn't suffer from this, but I don't think libraries have a great way to customise/improve their behaviour in that case at the moment.)
You do, but C++ includes RTTI separately from exceptions (dynamic_cast and typeid), so it's going to be there regardless. If you turn it off, then we're not talking about standard C++ anymore, but some subset.
When you throw and exception, C++ compilers add RTTI for those classes even when RTTI is separately disabled. You cannot call dynamic_cast and type_id but the generated code is the same.
I don't really see this as much of an issue given you have to have this RTTI to catch the exception properly.
My point is that RTTI is required by other things in the C++ standard, not just exceptions. So it's still "free" if we're only considering exceptions separately by themselves, and leave all other features as they are in standard C++ (and if you disable RTTI entirely, then you no longer have standard C++, since those other features also stop working).
If we consider the combo of exceptions+RTTI, then yes, dropping both at the same time lets you get rid of some further overhead (although again it's memory-only, not compute cycles). But then we have to discuss the lost benefits of those other features, not just exceptions alone, to judge whether doing so is worthwhile or not.
BTW, I may well be wrong here, but isn't RTTI generation for exceptions (when it's otherwise disabled) an example of "only pay for what you use"? i.e. wouldn't the compiler then only generate RTTI entries for those types that are either thrown somewhere, or are caught somewhere? If so, any library code that doesn't throw itself, and only needs to allow exceptions to pass through it, is still zero-overhead in that regard.
In terms of memory usage for similar Result type in C++, is there any way around allocating the space for both types (as opposed to Result in Rust, which just allocates enough space for the larger of the two)? My understanding is that there is not, although I'm still not very experienced with C++, so I wouldn't be surprised if I'm wrong
You can use a union + tag(which the same as Rust uses which is why they're called Tagged Unions) however you run into issues with constructors/destructors. You can do something similar with placement new/delete but it requires some hoops.
Honestly most error codes are small enough that you can just pay the hit for it on the stack and discard it once you've handled the good/bad case.
std::variant with c++17, but on linux at least you need to install gcc from git to have access to it so far.
boost::variant otherwise, but depending on the types used (if there aren't any that are nothrow default constructible) it allocates on assignment.
If you just want a result type and not a more general "rust enum", this repository does it (while only allocating space for the larger, I think) https://github.com/oktal/result/
And I don't think there's any reason you couldn't implement an equivalent of std::variant for previous versions of C++, I'm just not aware of any.
Heh, I implemented an in-house variant at work a week ago. As it turns out, I was able to use my variant class in a python extension I developed. Also, as it turns out, python is scary in what it lets you get away with when developing extensions.
It's still very bad, though. Exceptions control that badness. There's no excuse to avoid using a useful feature for reasons that actually worsen your code.
Exception handling costs absolutely nothing when not triggered (on most reasonably platforms these days, due to zero-cost exceptions), while that comparison instruction is still there, still taking its performance cost for every call of every function that could theoretically fail in the entire program.
If it's a hot path, branch prediction will almost certainly make the cost of that comparison negligible though. If it's not a hot path, does it matter?
The difference in cost is negligible compared to the difference in correctness. Vtables are so optimized (and often elided) and caches so efficient and comparisons are cheap and branch predictors so great that the cost of both of these is functionally 0 in all code I touch outside the tightest inner in physics sims and rendering pipelines. Even there the differences is statistically negligible.
On correctness, it is impossible to forget to deal with an exception, it is Tuesday when a C program forgets to check an error code. Did you know printf returns an error code?
There are ways to force these returns to be checked by the compiler, but a language needs to be structured around it to be sane otherwise you wind up with every printf in an if statement. Forced exception checking is almost as burdensome, anybody with a few years of java ought to agree or at least see where I am coming from.
Error handling is just plain hard. Only a few languages do it really well. I think either could be done right if baked in from the first language spec. But that is not where most code is written, most Java, C and C++ has to exist now. Clearly C has only error codes, and it has the most bugs. Java has exceptions and finally to force the dev to handle everything right there, which I think is a huge source of anti-patterns but better than error codes. C++ has exceptions and destructors which work the best of these, but still separate error handling from code execute all the time when it is only desirable most of the time.
But what you need is force-handled-correctly. If you think that programmers will correctly deconstruct partially constructed objects at every location where construction might fail, then you have far too much faith in our profession.
By that logic we should write hand optimized assembly. I think there was even a rant by Linus on GCC messing up the stack and he writes C. Clearly compilers shouldn't be trusted with important programs.
The behavior of exceptions in C++ is well-defined. But implementations do have bugs about many things that are well-defined - this is one of those cases.
A number of years ago, I hit against a GCC bug that generated an ICE (internal compiler error). Cause? Usage of an anonymous namespace in the global namespace. It was a short-lived regression, I think in the 4.1.2 series that came to me as a result of a supposedly minor OS update.
Very interesting. This is impossible in C++03; this must be related to the very useful changes in aggregate initialisation for C++11 (particularly useful as there's one great syntax for that and uniform initialisation).
If you're curious (as I was), this bug does not occur in clang, as you can demonstrate for yourself by following the link to wandbox.org in the original story and changing the compiler.
Not sure on the current Debian policy but this bug could certainly be framed as a potential security issue; exploiting the bug could result in resource exhaustion and denial of service.
I think it's not quite that severe; anybody who can connect to a server implemented in C++ that is compiled with gcc and initializes objects in the way described here could exploit the bug.
If you assign the initialized value to a variable then you get the expected behavior: https://wandbox.org/permlink/r6csgR44BZBWudLV. What exactly is the defined destruction behavior for an object that's never (necessarily) on the stack? Or is it actually required to be on the stack in this case?
In C++14, the spec specifies that a temporary object's destructor is called as the last step in evaluating the full expression that contains the point where it was created (C++14 §12.2).
I'm presuming it's the same in earlier versions of the language, but I don't have those specs handy ATM.
I'm more talking about the exception behavior looking at e.g. https://wandbox.org/permlink/S5paK3Z9NpeCv3I0 (which exhibits the same bug). I mean you're right, and the fact that the User object is temporary should presumably not keep the first Resource destructor from firing, just it seems weird. I guess in my defense, apparently it seems weird to GCC as well.
I don't have the specs either, but I do remember that in Design and Evolution of C++, Stroustrup discusses nailing down the lifetime of temporary objects. I believe it was in the first standard (i.e. 1998).
I imagine it's because those who maintain GCC have varying agendas for prioritizing their contributions.
The fact that is hasn't been fixed yet may be a signal that, in practice, this hasn't bitten anyone hard enough to motivate a patch submission. In which case, one could argue that the bugs are being tackled in a somewhat rational order.
It may be instructive to consider why you yourself haven't submitted a fix. Perhaps that reason is common across many of those capable of submitting a fix.
I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.
I hacked around in clang for a bit for a compiler project. I think it would have taken me weeks or months to get to the point where I was able to fix the equivalent bug (if it existed) in Clang. Compilers are big, hard to debug projects. And, IMHO, the Clang/LLVM code is much more approachable than GCC. Not all developers are hobbyists that can afford to detour from their work for weeks or months to ramp up on GCC to fix this.
I'd also point out that this is the kind of bug that could exist and be triggering in a lot of code as we speak but no one (besides the author of this blog post) have caught it. I think it's insane to ignore a bug like this.
>I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.
That's not my take on it. It doesn't read like an urge to fix it yourself, rather it's literally an urge to consider why you haven't, in order to understand why others also have not.
"Not all developers are hobbyists that can afford to detour from their work for weeks or months to ramp up on GCC to fix this" seems like an appropriate response and probably more or less the point that the GP wanted to make.
I don't think it does. ambrop7, DoofusOfDeath and dsharlet are IMO quite obviously talking about patching the GCC codebase and go out of their way to talk about the priorities of GCC maintainers, how approachable its codebase is and how hard compilers are to debug in general.
> I really disagree with this. I work on compilers, so I feel for the people who work on GCC, but, I think it is totally unreasonable to expect the average GCC user who might have encountered this bug to be capable of fixing this themselves.
I totally agree. GCC is reputed to be a complex beast, and few people have the spare time to get to the point where they can make the fix.
To clarify my original point: The bug apparently wasn't bad enough to cause anyone like yourself to either (a) fix it, or (b) pay someone else to fix it. So I'm at peace with this at a kind of meta/process level.
A meta-comment on this: had this been a bug in VC people here would shit on Microsoft and their non-compliance to standards, not caring and so on and so on. But since it's in GCC, people are discussing a language feature.
The RAII doctrine leaves you little choice, since the architects of C++ decided that constructors wouldn't be allowed to return NULL to indicate failure, or otherwise signal it in an easily-detectable manner.
That's why I don't use RAII. It comes with a massive amount of baggage.
Yep. There are any number of ways that C++ could have supported RAII without requiring either exceptions or a lot of unnecessary hacking, but they didn't.
C++ exceptions are dangerous, even when working as expected. In my opinion, C++ without exceptions (or the minimal for e.g. logging the error -division by 0, invalid memory access, etc.-) is a fine language.
Without exceptions you can't do RAII (because you can't do nontrivial work in constructors, because you have no way to report failure) which is probably C++'s one real selling point. (There are other reasons I'd say it isn't a fine language - mainly the lack of sum types and the way templates are only checked after full expansion (that one will at least be fixed if and when concepts make it into the language)).
RAII used to be incompatible with exceptions (I'm not sure whether anything changed for the better in C++17). The reason is that you can't throw exceptions from destructors, and e.g., on Linux, destruction of the file object (wrapping a file descriptor) can result in an error because close() may well return -1 setting errno to EIO. See `man 2 close` for details.
That's really not true. It takes a little discipline to ensure your constructor will succeed prior to calling it, but it's worth it. You generally want to make your destructor noexcept, so you get free implicit move optimizations.
I would like to point out that if you write code like this (that is, creating a temporary object only to have it instantly deleted), your code smells.
Backwards-compatibility notwithstanding, it would not be totally illogical for C++ to allow optimizing out construct-destruct chains even in the presence of side effects, in a similar manner to the existing copy elision behavior. After all, the object is unused, and the destructor should be merely undoing whatever the constructor did, so afterward it should be as if the object was never constructed.
If you rely on the side effects of normal construction, that's not so different from relying on the side effects of copy construction, which you can't already do. Hence you shouldn't do the first either.
So yes, it's a bug, and needs to be fixed, but regardless of that, you shouldn't write code like this in the first place.
EDIT: See comment below, apparently the bug comes up in more cases than those I referred to here.
I don't think it seems like a code smell at all. For example, I could be adding together multiple vectors, some of which are created as temporaries.
auto total_offset = Vector{x_offset, y_offset} + global_offset;
Granted, in this case, there are no resources being held by such a Vector class, but having immediately-destroyed temporary objects is not necessarily a code smell.
That's definitely not "immediately destroyed", there's operator+ being called between construction and destruction.
I didn't realize this can trigger the bug though, so if it does, I should clarify I didn't mean to include it in my comment. I was only referring to cases where there is no intervening operation between construction and destruction.
It's a minimal testcase; minimal. I thought the blog post was pretty clear that it had nothing to do with how the object was used after (e.g. if it was immediately destructed or not) and even linked to a real world example that would disprove that.
Hence why your holier than thou attitude about not writing smelly code is just asinine and not helpful. It plays into a mindset that people who write "good code" don't have to worry about 'esoteric' bug reports (e.g. like security advisories).
There's probably far more reasonable cases of temporaries being immediately destructed than you'd expect.
One that springs to mind is gtest. It defines a bunch of macros like ASSERT_TRUE that return a temporary object that has operator<< overloads to let you add additional information to the assertion failure. For example:
ASSERT_NE(-1, open("/foo", O_RDONLY)) << "failed to open file: " << strerror(errno);
It's perfectly legitimate to just go ASSERT_TRUE(1 != 2), though.
> I would like to point out that if you write code like this (that is, creating a temporary object only to have it instantly deleted), your code smells.
Not necessarily. What if all you want are the side effects of the object?
In particular I/O. Either disk or network.
Creating the object opens the connection, destroying it closes the connection. And you use the object in between to write data.
Or, don't assign the new object to anything, and simply write the data you pass in to it, and close the stream.
I was explaining that you could use the object in two ways. (If you only ever used it the second way then you would just make it static, so I was giving the first way as a reason why it would not be static.)
Instantly, as in next line of code? Yeah, probable human error (or auto-generated code).
Without being used? Not surprising. It is surprisingly common to allocate a variable, pass it to a function, and have the call never actually use the variable. Unwind, and the variable is destructed without use.
I've seen other nasty framework errors exposed by such (non-)usage patterns.
Only compiler bugs I've seen in C++ have all been around exceptions. I think this is just another reason towards force-handled error codes. You don't have a whole new control flow that needs to be verified and is much simpler from an implementation/runtime perspective.