Herb Sutter: When a language’s creator and its owners strongly disagree: C++26 post-DIS, historical experience, and paths forward
60 points by mort
60 points by mort
Interesting writeup.
However, in its references to Rust, I think this mischaracterizes the role of Graydon Hoare. He explicitly did not want to be BDFL or have special sway over the language, and one reason he stepped back was to avoid ever causing a situation like the one C++ now finds itself in.
In fact, Graydon would usually contradict claims portraying him as "the creator" of Rust. Yes, he started the project, but Rust 1.0 is the brainchild of many people.
I'm pretty well aware of this bit of Rust history, and I think I agree with both your characterization and that in the article. I'm not sure they're actually incongruent. It doesn't say Graydon ever was or wanted to be a BDFL, it even lays out that various governance models, BDFL being just one of them, can all lead to this split:
No governance model has prevented such a disagreement: it has happened under ANSI and ISO committees, under ECMA, under benevolent dictators, under open-source communities, and even in corporately owned languages.
The article also mentions the creator stepping back as one of the ways to resolve this issue:
When neither side yields, the usual end result is “fork” or “quit.” Perl forked permanently; Python temporarily lost its creator’s involvement (he later resumed participating but no longer as BDFL); Pascal, Swift, and Java lost their creators’ involvement permanently.
They don't list Rust here, perhaps for a lack of detail knowledge, but I'd characterize the result as "quit", too. Graydon stepped back, leaving entirely for a while and then returning slowly, but never exerting a great deal of influence over the language's direction again.
That being said, I also agree that Rust's case is unique, insofar as none of the other listed languages had their creator reject the BDFL role as early and as strongly as Graydon did.
I think this whole situation is incredibly politically interesting. The C++ standardization process is fascinating.
One thing I've been thinking about regarding the criticisms of concepts is: to me, an "obviously good", incremental change to the language would've been to add better assertions. Standardize something like Google's CHECK and DCHECK, with proper language support. I imagine a solution where C++ defines 3 build modes: debug, release and release-unchecked, then introduces modernized assert functions, std::check(bool) and std::dcheck(bool).
std::check(bool) runs in modes debug and release, it throws a std::contract_violation (or abort or whatever) if passed a false-y valuestd::dcheck(bool) runs only in debug mode, it also throws a std::contract_violation or whatever if passed a false-y value but is for slower checks which should only be run in debug modestd isn't new, look at std::launder)I personally would love something like this; I wouldn't have to try to remember whether I was meant to define/undefine NDEBUG or _NDEBUG or _DEBUG, moving more things away from macros is a good thing, it would be nice to have a standard way to define heavier debug-only checks and lighter "you probably want to run these in release mode too" checks, etc.
Yet it runs foul of one of the major issues which C++26 Contracts detractors bring up, which is that behavior changes depending on compiler settings for the particular TU and that checking behavior might therefore be different depending on the compiler settings for the TU whose implementation of a function happens to be picked.
I don't understand why this is worse for C++26 Contracts than for my obvious and minimal version, or C-style assert() for that matter. Is there anyone who could care to explain? ODR violations due to compiler settings between TUs is obviously a big problem, but it has been an obvious big problem for most of the language for the past 30 years, why does it matter more now?
Throwing an exception in response to an assertion violation seems weird to me. An assertion failure means, "Some fundamental invariant of this program was supposed to be impossible to violate, but it happened anyway. Presumably everything we though we knew was a lie?"
The correct response to an assertion failure is an abort, or some dire last case mechanism like killing a thread and notifying a handler supervising that thread. Erlang/BEAM and Rust essentially have versions of the latter. Erlang gets away with this because Erlang processes have isolated memory spaces and because processor supervisor trees are standard. Rust mostly uses this mechanism when running tests, to catch assertion violations in a test and report them nicely.
But exceptions are essentially an error flow, not a "panic-like flow". They don't feel like a good match when your program is in a known-broken state where fundamental assumptions have been violated.
(Not that I have a dog in the C++ fight.)
OpenStep’s NSAssert macro threw an NSInternalConsistencyException if the assertion failed. So we have over 30 years of this model being tested in production at scale (almost every OS X and iOS app will do this somewhere: inside system libraries if nowhere else).
One of the objections to aborting comes from safety-critical systems (the place that P2900 claimed to be helping, though the fact that it makes source-level reasoning non-local means it’s a disaster in that domain). Stopping is often more dangerous than continuing in an erroneous state in such systems, because the software is controlling some physical process.
Safety-critical systems tend to be so specialized that it's hard to talk about them as a class. There are automobile control systems, medical implants, avionics, etc. They each have different kinds of fundamental correctness assumptions, and different sorts of "safe default" actions to take. On top of that, even if these systems are written in a language like C, they're written in a highly-specialized way, using specific practices and tools.
My own limited experience is with mobile robotics, which sometimes have actuators strong enough to punch through walls—or through engineers standing too close. My coworkers had actual scars from previous jobs.
So let's say your safety critical system suffers an assertion failure. Some state, which you were sure was impossible, has been reached. One of your code's fundamental assertions about the universe has turned out to be wrong. Maybe your robot's current arm position is recorded as 36778912 out of a possible 360 degrees. (Oops.) Maybe your autopilot has been instructed to set the flight level to 2 miles below current ground level, when higher level routines were never supposed to do this. Maybe your data structure contains -6 elements.
Something has gone deeply wrong. It's possible that your heap or your data structures are corrupt. Or perhaps your program logic is fundamentally invalid. You have some very tough engineering decisions to make, because now you know you're operating on "impossible" state or that you're using known-bad logic.
The correct answer varies depending on what you're building. Frequently, your first goal should be to escape the "blast radius" of the damage. If your data structure contains -6 elements, then your probably shouldn't be using that data structure. If your fundamental system state (like actuator positions) contains blatantly impossible numbers, then you can't really make decisions based on them. And looming over all of this like two specters are the possibility of memory corruption, and the historically high rates of bugs in error-recovery code.
Erlang's approach is wiser than most: Each process is isolated. When things go too wrong, you assume the entire process is untrustworthy, kill it, and return to a supervising handler. You still have hard decisions to make, and there's no one-size-fits-all answer. Maybe you want to run a specialized power-down routine for your robot (which may itself require complex code or tradeoffs). Maybe you want to gracefully disengage the autopilot and notify the user that they need to take over, because your autopilot is clearly defective. You need to have some story on how you fail to a safe state despite running on bad code and/or impossible data.
(For what it's worth, I've read enough crash reporter stack traces to be convinced that most desktop software is buggy garbage that barely limps along on a good day. And I fear that basically all C/C++ programs bigger than 1,000-line embedded hardware controllers are unfixably buggy, as the recent endless deluge of CVEs suggests. We have not been doing well as an industry.)
Thank you for this nuanced and wonderful exposition. It really does exemplify that no one-size fits all!
There is always a well-known solution to every human problem - neat, plausible, and wrong.
- H. L. Mencken
I didn't write it because it wasn't relevant to my core point and my comment was already getting pretty long, but my idea is actually that std::contract_violation should not be an exception, aka not inherit from std::exception. That would mean it can be caught if you actually want to (just like you can catch panic in e.g Rust or Go), but it won't be caught by typical existing catch-all exception handlers.
And when compiling with -fnoexcept it's just be a call to std::terminate anyway.
While catching panic is mostly done in tests or at the toplevel of an async loop, Rust does allow caching panics at arbitrary locations (as long as panic = 'unwind', which is the default).
Also as a few other data points, in Java and Python assert raises AssertionError which is a standard Exception, afaik ruby doesn't have assertions per se but the usual way is to raise a regular exception (e.g. raise error if condition)
I view asserts more as a "my assumption at this point in the program is that this should be true." And yes, I do run with asserts in production (at least, on my public server). And yes, there have been a few times (once or twice if I recall) where an assert triggered, and upon deeper investigation, I realized the assertion was wrong. But note, this is using C's assert(). I also use assert() in a contract-like mechanism in my C code (mostly, pointer parameters to functions should never be NULL, stuff like that).
Funny how the cited Stroustrup PDF with objections to contracts seems to have its majority (Appendix: Documentation) be completely AI-generated
I can't understand how anyone can look at that and take his objections seriously if Stroustrup didn't even find them important enough to actually write them down himself: https://www.stroustrup.com/Contracts_fundamental_flaws.pdf
Damn at first I was looking at the references, and I didn't know what you were saying. Then I see the Appendix right before, and it does reek of "Claudish".
I mean yeah Claude does know a lot of C++ (and Python and SQL and HTML ...). The complexity of C++ -- where famously nobody can fit it in their head -- does seem like a ripe target for LLMs.
But still it is surprising to me that Stroustrop (or maybe one of the co-authors) would do that in official communications.
People have sadly become so lazy to type in their own words. It has become exhausting with coworkers because I usually get excited to receive a message by someone who send me a summary of something, or pitch an idea, only to be disappointed by them just sending me a long block of slop.
The issue I see is that this leads to people stopping from sitting down and really thinking about a concept intimately. Everything has to go fast, no time to wait or reflect. But without reflection there is no insight, and without insight there is no progress.
The constant push for new features in C++ seems to me to be more driven by existential fear rather than actual technical merit. This fear is driven by the "javaification" of C++ -- that it may one day become a "don't write new things in this," kind of language. That is scary to the people with the power to make decisions because that means their code is all of a sudden "unsafe legacy code." That statement, regardless of its technical truth, deters stakeholders enough to foment panic.
I'm going to go out on a limb here and assume things about C++ contracts that I do not know to be true, so if I'm wrong, please correct me.
There are two benefits I can see to standardizing contracts:
To address #1, this does not need to be in the standard to be done, as stated explicitly in the write-up.
As for #2, semantics that intuitively become compilation errors are a) practically speaking unstandardizable without becoming a turing-complete combinatorial explosion of compiler switches and b) not necessary to standardize.
Do not mention the potential benefit of compiler optimization. We're already scared enough of restrict.
This is not to say that contracts should not be implemented, since that depends entirely on how you define "should."
As above, the real reason this is being pushed is that major parties want to be able to declare to their investors and institutional customers that their code is "safe at the language level, just like Rust" with the minimum amount of resources expended for the maximum amount of checkboxes ticked. It is not a stretch to call the committee's rhetoric propagandous.
From my perspective, it looks like C++ is a 41-year-old man wearing a jorts and a sideways baseball cap.
Edit: reading this back, it reads more inflammitorily than I meant it to. I don't mean to paint any big bad wolves, nor do I mean to discount the real technical merits of contracts.
Option 2 is explicitly out of scope of P2900. Doing it well requires defining a subset of the language and adding ghost state for propagating proof obligations and axioms. SPARK Ada and Verus in Rust are both nice examples of this working very well in other languages. P2900 makes it largely impossible for C++ to get such a system. Which is a shame because that’s the clearest path I see for C++ to be able to evolve to being a safe language (some of the papers using Verus to prove properties across unsafe blocks in Rust are very interesting and that model for C++ would enable local reasoning).
the clearest path I see for C++ to be able to evolve to being a safe language
We have differing viewpoints here, so I'd be interested to hear why you think C++ needs to evolve at all. My personal tastes may not allow me to see why there is a good reason that C++ needs more features.
As I said in another comment, what it looks like to me is that institutions with stake in C++ benefit from C++ becoming "safe," even if the practical benefits of whatever that means have not been evidenced on large, existing codebases at existing organizations, as far as I know.
I’m not sure who the major parties are here?
On optimization though: this was something I asked many questions about. the compiler is not allowed to treat a contract predicate as a proof of state (e.g it can’t say “the precondition says X, therefore X is true”), nor can it back propagate UB to remove the guards - the latter is accomplished by the introduction of observable checkpoints (I cannot recall the exact term off hand), these also feed into the hardened library specifications and erroneous behavior.
the compiler is not allowed to treat a contract predicate as a proof of state ... nor can it back propagate UB to remove the guards
I'm glad this is the case. Generally, I concur that human programmer shouldn't be trusted to predict the state of their programs in such an abstract way.
As for who the major parties are, I admit this may come from an utter misunderstanding of committee politics, and assumptions I may not have the grounds to make. My understanding is that the majority of the committee members are hired by major corporations who have stake in and want influence over what happens to the C++ language. I don't think that's a bad thing, but if you're paid by a company to achieve their goals, you will probably do things to that end. If you work for company X, and company X is losing clients because their code is all of a sudden unsafe, you may want something to show them that there's progress being made.
Feel free to correct me if the narrative I've concocted does not mirror reality.