A bug in GCC that may cause memory leaks in valid C++ programs
akrzemi1.wordpress.com
akrzemi1.wordpress.com
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.
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++.
(Of course you could do the same thing as Rust and expose only a static creator function and hide the actual constructor.)
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.
I don't really see this as much of an issue given you have to have this RTTI to catch the exception properly.
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.
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.
If you use optional<T> you have to explicitly write that, and turning off exceptions means you don't have to write noexcept everywhere.
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.
For example the following code will compile differently depending on whether goo() can throw.
struct k { void p[4]; };
void goo(k s);
k foo() { k r; k s; goo(&s); r = s; return r; }
If goo() doesn't throw the extra variable s can be eliminated.
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?
Not when the exception is triggered
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.
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.
I'm not going back to explicit error handling.
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.
[0] match whatever { Ok(v) => v, Err(e) => return Err(From::from(e)) }
Btw: Is it correct that this won't get fixed in Debian because it isn't a security issue?
I'm presuming it's the same in earlier versions of the language, but I don't have those specs handy ATM.
There is no ambiguity in the spec here; this is a GCC bug.
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 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.
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 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.
That's why I don't use RAII. It comes with a massive amount of baggage.
User u{make_1(), make_2()};
process(u);
causes the first resource to be correctly destroyed.Sad to know that after all that time compilers still can't get even the core semantics right.
Trusting Trust.
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.
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.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.
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).
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.> That's definitely not "immediately destroyed", there's operator<< being called between construction and destruction.
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.
It's a perfectly reasonable use case.
That's not what I was talking about. See the original comment and follow-ups.
You didn't read my next sentence.
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.)
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.