It doesn't.
Exercise for the reader: detect evaluation interleaving without exceptions.
> If your code throws so many exceptions that not freeing the memory allocated during that code point becomes a problem
It need not manifest itself as a mere memory leak.
Exercise for the reader: make something happen beside a memory leak.
Yes it does. Of course you can detect different evaluation orders using side effects, but the fact that evaluation order is unspecified isn't generally a problem: if you write code like `a() + b()` and a() and b() has side-effects that depend on the order they're evaluated in, that is just a bad way of writing code. In any language.
The point here is that the compiler is allowed to interleave subexpressions of the two different arguments which causes a memory leak in seemingly safe code. That is a very unreasonable thing for the compiler to do, which is why it is a footgun. But not that it can ONLY happen if the second function throws in between the construction of the object with "new" and the construction of the shared_ptr: if the second function doesn't throw an exception, the construction of shared_ptr is guaranteed to happen.
Incidentally: those "exercise for the reader" comments are incredibly obnoxious.
generally is the key word here.
> if you write code like `a() + b()`
The code doesn't have to look like that. Add e.g. some arguments...
f(**x, g());
The number of stars evaluated before 'g()' (0, 1 or 2) varies between compilers, and the program I was working on would break if only one * was evaluated before g.> Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, order of evaluation of arguments to a function). Where possible, this International Standard defines a set of allowable behaviors. These define the nondeterministic aspects of the abstract machine.
> Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer). [ Note: this International Standard imposes no requirements on the behavior of programs that contain undefined behavior. —end note ]
Unspecified means that you can't rely on the order of evaluation. "Anything" may not happen; the compiler controls the sequencing.
Another favorite “criticism” I have of C++ goes along the lines of “but someone could overload operator+ to be division!” I have only seen that as a contrived critic’s example, or a joke.
Actually, that’s what you are supposed to do, as this is the only way for a constructor to let the caller know that something went wrong.
The factory could return std::optional<Foo> or std::unique_ptr<Foo> or std::variant<Foo, Error> or absl::StatusOr<Foo> .
It's an improvement in terms of code reuse but not in semantic simplification basically.
I'm not sure what you mean. Here's an example with the output from std::optional being unwrapped and put in a struct member: https://godbolt.org/z/Ms4Kar18j
Also there's opt.value_or() which can use a default value in the case opt is nullopt.
>It also replaces an automated and enforced action by the compiler (exception being thrown) by a need for a manual check which feels very 1975
I think remram's goal was to avoid exceptions. So this is a criticism of remram's goal rather than the method to achieve that goal.
Also, std::optional can integrate with exception-producing code because opt.value() throws an exception if opt is nullopt.
And now you have to do this for every type that have some amount of preconditions. I don't understand in which universe this is sane - more code == more bugs.
e.g. here's what one would write with exceptions: https://godbolt.org/z/evMsh6383 (and, to be honest, if one is writing e.g. a command-line tool and not a reactive gui app, most likely https://godbolt.org/z/eW8YM5Gz4 as your OS will catch the thing anyways and then it's just a `coredumpctl gdb` / "open in debugger" away) ; you get the same guarantee of never having an invalid Foo but at much less mental cost.
It also prevents you of having aggregates as all the "parent" owner class will need their own wrapping in optional + private ctor, in case a sub-sub-sub-sub field would fail.
e.g. given
struct DomainObject {
Foo mainFoo;
Foo secondaryFoo;
int whatever;
};
struct GameState {
std::vector<DomainObject> objects;
};
now you can't just do DomainObject{initForMainFoo, initForSecondaryFoo, 123}; or DomainObject{.mainFoo = "", .secondaryFoo = "", .whatever = 123}; anymore, even less GameState{{DomainObject{...}, DomainObject{...}}; which is how modern C++ is meant to be usedOn the other hand return values (using something like a try/either monad) have the property of forcing the developer to think about the unhappy path in every layer. Maybe a bit more code, but the quality of diagnostics - priceless.
IMHO exceptions/panics should be reserved for bugs only (eg assertions), and return values for all the other error handling caused by user input or environment.
but it's the whole point ! that's exactly why exceptions are good !
> to a place where there is so little context that the final error message presented to the user is near useless.
there are only two valid cases: there's an error that you know you are able to recover from, you did recover, and the users does not see a message and never knows that an error happened, OR the user sees the message "The program has encountered an error. A backup of your data has been saved in c:/foo/. The program will now exit.".
Anything else makes software terrible to use. If you have logging and recovery to do, use your logging architecture and core dumps, don't misuse exceptions for that.
> On the other hand return values (using something like a try/either monad) have the property of forcing the developer to think about the unhappy path in every layer. Maybe a bit more code, but the quality of diagnostics - priceless.
Except what happens in practice is that people just put panics everywhere because most of the time there is no meaningful action one can take at the layer where the implementation happens. Actual examples in servo: https://pastebin.com/0DAFU9vS
I also invite you to run the following command:
$ cd ~/.cargo
$ rg panic! | wc -l
I get 2987 as a result. Choosing to program like this instead of using exceptions which at least can be recovered by the person who will use your lib, is literally hateful towards users.I've not seen Go code to be meaningfully different either.
> IMHO exceptions/panics should be reserved for bugs only (eg assertions), and return values for all the other error handling caused by user input or environment.
Sure, this is the community consensus in C++. It's the Python people who use exceptions to get out of for loops :)
But the most important rule is that - invalid state must not be representable in the program. There must be no way to get an "invalid" object (unless the object being invalid is part of its possible domain, but this is exceedingly rare when one isn't interfacing with a 1985 C API)
Good for what? For crashing the program with a stacktrace? Probably. But for regular error handling - that's a terrible thing. They break normal control flow by adding a parallel / alternative control flow. Now with them you have to think on what happens if any of the code you call suddenly throws an exception and the number of potential control flow paths increases drastically. They are not referentially transparent and it is nearly impossible to do functional programming with them (they introduce side effects everywhere). You can also end up in a really bad situation when a destructor throws (typically crash).
> I get 2987 as a result. Choosing to program like this instead of using exceptions which at least can be recovered by the person who will use your lib, is literally hateful towards users.
But panics are Rust equivalent of exceptions. That's the whole point of them. They are used for exceptional cases, which are impossible to recover from (which means the only way to deal with is to dump core and terminate) and most of the time preventable (you can always avoid dividing by zero or going out of array bounds).
It looks like we agree they should not be used for regular errors, like e.g. file not found. I'd not like my word processor to terminate with a core dump when it couldn't load the file from the disk. In this case return values are way better error handling mechanism.
> invalid state must not be representable in the program
Returning an option/try/either strikes that goal just as well as exceptions.
No, not really. Either the allocation followed the happy path, or one of the expected failure modes kicked in.
And the supposed footgun example boiled down to failing to handle one of the failure modes.
I mean, think about it: std::make_shared is a factory method, isn't it?
However mind that if you don't throw from C++'s perspective the object is fully created, so that the destructor will run.
If you throw an exception from the contractor, the object won't be fully created.
Yeah there's definitely criticisms you can throw at C++, but this always struck me as stupid. You could also write an `add` method that divides in Java.
I've never liked the idea of 'operators' in the first place, tbh. Scheme opened my eyes in that they're all just procedures. And then with Smalltalk and Scala I saw that they can all just be methods as well.
I don't see how that is so. C++ 17 allows new Widget to complete and then the priority() call to execute and throw before both are passed to shared_ptr(), thus creating a leak. Your cited example [1] doesn't leak because both arguments are shared_ptr. Seems to me that C++ 17 does indeed solve this latter case but not the former.
> evaluations of A and B are indeterminately sequenced: they may be performed in any order but may not overlap: either A will be complete before B, or B will be complete before A. The order may be the opposite the next time the same expression is evaluated.
And:
> 21) Every expression in a comma-separated list of expressions in a parenthesized initializer is evaluated as if for a function call (indeterminately-sequenced)
Emphasis mine. What the above basically says is that in the case of some function `f(A, B)`, the arguments `A`, and `B`, are what's known as "indeterminately-sequenced" -- this mean that their execution cannot be interleaved (overlap) -- but they still individually execute in a non-deterministic order (A before B, and sometimes B before A)!
With that said, the good news is that B can now never throw in the middle of A, which is precisely what we have in OP's example.
Looks like footguns.com is taken but footguns.org is still available.
* Start with C++20 unless you have a very good reason not to. Not only does it obviate crufty old things like SFINAE (concepts!), but it includes a ton of usability fixes (e.g. reflexive operator== -> I only have to write the code for MyType == MyOtherType in order to get both that version and MyOtherType == MyType). A lot of lambda behavior has really been cleaned up too (e.g. capturing `this` has fewer corner cases).
* start with Google's coding guidelines [0], as they've been developed to avoid many footguns (the bottom line depends on it). Once you understand things better, keep removing these rules (e.g. about exceptions, const& parameters; many of these rules are good for big teams, but annoying to follow for individuals)
* The CppCon "back to basics" talks are absolute goldmines, and the speakers usually tell you if some technique has been outdated as of the recording. Some things in certain books, although valuable, may be outdated based on later revisions to the language.
* The best notes to take are usually some structure list followed by a bunch of godbolt links with examples, e.g. this one I made to demonstrate the reflexive operator== behavior [1]
[0] https://google.github.io/styleguide/cppguide.html [1] https://godbolt.org/z/scPdcaWT7
priority() is not passed to shared_ptr(), only to processWidget().
processWidget(new Widget, priority());
I think I saw the above code elsewhere.
However in the Effective C++ example, the `shared_ptr` constructor gave a false sense of security as it seemed the `new`-ed `Widget` was always managed by the smart pointer from its allocation.
Another example is when undefined behavior occurs, like (iirc) signed integer overflow. Compilers can technically do anything they want, including weird things. But how often does that actually happen?
int a;
...
int b = a
if (a + 1 > b){
foo();
}
Then compilers will use the fact that integer overflow is undefined behavior. This allows the compiler to assume the `if` statement will always be true. And hence it removes this check.This isn't just the compiler removing a check the programmer wanted. This could occur because of constants, where other constants make the check meaningful. It could also occur after in-lining a specific function call.
Really, the only case where this optimization would be bad is if the author was trying to detect overflow. Sadly, detecting if something would cause undefined behavior in C code is hard. Because you cannot try the thing.
Turning optimizations on doesn't suddenly undefine any behavior, it just changes what the compiler does when encountering some of those behaviors (among other, non-UB based optimizations). A non-optimizing compiler and an optimizing compiler both have to conform to the standard.
It is simply not possible to write an optimizing compiler in which the evaluation order is truly indeterminate yet always the same.
{
auto& a = <expression>
auto& b = <expression>
foo(a, b);
}
To foo(<expression>, <expression>);
Could be an optimization.Edit: modified the code a bit to make the two samples more similar.
[0] http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0145r3....
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p040...
> We do not believe that such a nondeterminism brings any substantial added optimization benefit, but it does perpetuate the confusion and hazards around order of evaluations in function calls.It perpetuates unnecessary confusion around brace-initialization vs. direct initialization using parenthesis.
> We found that some entries in the benchmark suite ran slower, others ran faster compared to the scenario where the evaluation of the argument list is left unspecified. The variation is between -4% and +4%. It is worth noting that these results are for the worst case scenario where the optimizers have not yet been updated to be aware of, and take advantage of the new evaluation rules and they are blindly forced to evaluate function calls from left to right. It is clear that the left-to-right evaluation strategy is triggering new optimization paths (different inlining decisions and different register allocation) affecting the variations in the benchmark performance. It appears those opportunities have not traditionally been exploited, even though permitted under the unspecified order regime.
Unfortunately, they seem to have been overruled by the Core Working Group [1], for reasons I have not been able to dig up.
[0] http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0145r3....
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p040...
> In this case, it's pretty easy to see that 3 will have to be printed after both 1 and 2, but it's unclear whether 1 or 2 will be printed first. The reason for this is evaluation order: in order to evaluate one + two, we'll need to evaluate both one and two. But there is nothing telling GHC which of those thunks should be evaluated first, and therefore GHC is at full liberty to choose whichever thunk to evaluate first.
Note that in Haskell evaluation order is not guaranteed anywhere, even between separate statements, whereas in C++ it is basically guaranteed everywhere except function calls.
As for paranoia about undefined behavior? Compilers won't emit a program to call you a pizza or format your disk. There are risks, but those aren't real ones.
how is that different from undefined?
Unspecified behavior is bounded both in space and time. Unspecified behavior may only affect program semantics within the statement in which it is engendered and may only be observed within the region of memory that is written to or read from.
It is often said that undefined behavior is invalid, and one can sympathize with that statement as it's commonly repeated, but it's not true. All the standard says about undefined behavior is that the standard imposes no requirements about program behavior, it does not state that the program is invalid or that the program must behave in some kind of invalid manner. Almost all non-trivial C++ programs engender undefined behavior in a manner documented by the compiler and do so in a safe and principled way.
It certainly is the case that most compiler writers have decided this is okay. And I personally tend to agree with them.
> The second one is there since this is C++, priority() might raise an exception, meaning that new Widget will be called, but never passed to std::shared_ptr, and thus never deleted!
Is now impossible (thank God!). See a full SO discussion here[1]. Stuff like this makes me so happy I don't write C++ anymore, but the gist of it is (from the standard):
> For each function invocation F, for every evaluation A that occurs within F and every evaluation B that does not occur within F but is evaluated on the same thread and as part of the same signal handler (if any), either A is sequenced before B or B is sequenced before A.
[1] https://stackoverflow.com/questions/38501587/what-are-the-ev...