The C++ Standard Library Has Been Walking Itself Back for Fifteen Years, and the Receipts Are Public
59 points by tmcb
59 points by tmcb
Gah. If you're going to write a post like this, please write it. I'm sure you made the list yourself, but sending that list through an LLM and spewing the result onto a webpage for human eyes is so massively rude. If I have to read one more sentence about how every "working engineer" is told to avoid "feature X" on their "first day" I'm gonna lose it.
And the shame is that there's so much here to say but you aren't saying any of it! You're a person who made this post for a reason, tell me that reason. Surely you yourself got mad at C++ for one of these? Surely you were confused by these features? The reason they're so bad isn't just the objective design failures but the impacts they have on us. Did you ever get roasted on slack for using std::iterator? Did you ever just avoid writing a cast because reinterpret_cast is 16 characters and would've made the formatting of a line slightly worse? These are the kinds of things I'd rather see contributed to Lobsters than some pithy generated list of descriptions.
And if you don't have stories like that to contribute, then don't contribute them! But don't force a GPU to spend matrix multiplications forming the same sentence 10 times. Annotate the parts you do have comments on, write the rest in a table with bullet points, stop wasting my time.
None of this reads like it was written by an llm to me.
Some of the more obvious tells were edited out after it was removed from HN and r/Programming.
No human edited this. The original lists and argument were almost certainly made by a human, but not the final text. I don't want to be dismissive, I appreciate that you cared enough to comment, but the AI-isms are really obvious here. While these could be signs of lazy writing, there are just too many for someone to produce by typing into a keyboard or speaking aloud without them also thinking that maybe, perhaps, perchance even, it could be a little too much.
For me the most annoying are the pithy thought-ending cliches at the end of paragraphs. Not everything needs to have a point made about it. I do find myself writing like this at times, but I try to stop and actually consider whether those points are meaningful. And if they are, then I'll try to actually argue for them instead of just leaving the statements out to dry as though they can support everything written before.
This article rehashes ad absurdum the same points in the original premise over and over without substantially growing a larger argument beyond the point that C++ doesn't remove things and doesn't improve.
I enjoyed the recent article It's Not Just X. it's Y that discussed how AI uses the language of reasoning, but doesn't actually do any. I care a lot more about the reasoning being correct than it sounding correct, and infesting human communication with these AI-isms makes it easier to sit back and leave your brain on pause even in your own writing.
Here's a quote from almost every paragraph. I skipped plenty.
std::functionshipped in C++11. The committee spent fifteen years shipping the wrappers that should replace it.
Sometimes the sentence is what every senior engineer tells every junior engineer on day one ("never reach for that, here is what to use instead"). And sometimes the sentence cannot be written into the standard at all
None of these are arguments. They are admissions in writing.
The replacement is the lambda, a language feature that landed two cycles earlier and made the entire adapter framework irrelevant.
Eighteen years of standard text spent un-shipping an exception model.
The replacement is "define the five typedefs yourself", which is what most engineers were doing anyway because inheriting from
std::iteratornever gave you anything useful.
The replacement is "use
alignas(T) std::byte[sizeof(T)]directly", which is what the standard probably should have shipped in the first place.
Trigraphs were removed in C++17 after thirty years of being a wart on the language.
Twelve years of standard text spent un-shipping a feature nobody used.
Every one of those is the same admission written differently: "the previous design did not work, here is the next one."
The original
std::functionis still in the standard. Dargo's table tells you to avoid it. So does every working C++ codebase you can audit.
These features are still in the standard. None of them are formally deprecated. Every senior C++ engineer in the industry will tell every junior engineer to avoid them on the first day of the job.
Outside the standard the replacement is Boost.Regex, RE2, or PCRE2. Production code uses one of those. The standard
std::regexexists for tutorials.
In the meantime, every working low-latency codebase uses thread pools,
std::threaddirectly, or platform-specific async primitives.std::asyncexists in the standard so that introductory textbooks have something to write about.
The committee will not write that sentence. It is also the sentence every working engineer is told as soon as they ship printf-style debug output in a code review.
std::listis not deprecated. It exists in the standard. Every working C++ engineer is told never to reach for it.
Until then,
std::dequeships in every standard library with the same poor cache behaviour everyone has known about for twenty years.
current cppreference text says implementations "don't appear to have any special code" beyond a plain container. Eigen, xtensor, and Blaze fill the niche.
std::valarrayis in the standard for archaeological reasons.
The fix would be a rename, which the committee will not do, so the trap remains in the standard. Every working engineer learns to write `std::deque<bool> or to use a different container.
Each of these has named expert commentary calling the design a defect, and none of them are formally deprecated. They are the standard library you ship to production and route around with your own helpers, your own conventions, and your own code-review rules.
The committee deprecated a feature, the affected community objected, the committee walked the deprecation back, and now there is a paper to walk part of the walk-back back. This is what fifteen years of standards work on an eight-letter keyword looks like.
These are the containers a C++ beginner reaches for on the first day of writing real code. Three of them are demonstrably wrong by the standards everyone else has agreed on. We have receipts.
The C++ committee added
std::flat_mapandstd::flat_setin C++23 via P0429R9 but cannot fixstd::unordered_mapitself. The defaults stay broken.
B-trees have beaten red-black trees for in-memory containers on real hardware since roughly the time the iPhone shipped.
... the C++ standard library's default is the slow one, and the slowness is large enough to swamp every other factor in the benchmark.
The point is not that Rust the language is 58x faster than C++. The point is that Rust's standard library shipped with the right defaults the first time ...
The difference between the two is structural. The C++ committee knows it is structural. The fix is not formally on the table.
The pattern under all three tiers is the same.
This is not a critique of the committee's individual members, most of whom are doing the hardest possible job in the most thankless possible venue. It is a critique of ...
The standard library is now a layer cake of fifteen years of "do not use that, use this instead", and the working engineer's job is to know the dates of the bad layers.
The committee is not only failing to remove bad features. It is also continuously adding new ones that no working engineer asked for, ...
The paper is not a casual blog post. It is a numbered WG21 document from the man whose name is on the language
No production codebase is converting to it. The only group that benefits is the proposers, whose committee work now carries the prestige of "got a feature into C++26"", and the field of consultants and trainers who will now teach courses on it.
^ This one is just rude to the committee
That is the Vasa problem in operational form. Each cannon makes sense in isolation. The ship is what cannot stay upright.
Every new feature shipped means new test matrices, new conformance bugs, new interaction surfaces with every other feature in the same standard, and new entries in the bug tracker that the next generation of maintainers will inherit.
The committee adds; the implementers absorb; the ABI commitment then prevents anyone from ever cleaning up.
The Vasa paper diagnosed this in 2018. The pattern has continued.
Python had the 2-to-3 break. Java has a deprecation tooling system with
@Deprecated(forRemoval=true). Rust has editions. C# walked away from CAS, Remoting, and BinaryFormatter when it moved from .NET Framework to .NET Core.
Python deleted twenty-plus standard library modules in a single PEP.
... but for the class of mistakes editions can address, Rust addresses them by design.
Three patterns shipped, two are now vestigial, and Microsoft will tell you in writing which one to use.
JavaScript's escape valve is "do not ship it past Stage 4", not "remove it later", and the cost is that the language has accreted parallel mechanisms (
var,let, ...) that nobody can throw away.
Go's design choice is to live with the mistakes rather than ship the breakage. C++'s situation is the same outcome arrived at by a different mechanism.
C++ has shipped
std::regex,std::unordered_map, ..., and almost none of them have been removed.
The standard library's ABI is enforced by the dynamic linker, not by polite request ...
This is the structural argument and it is what makes C++ different.
AI, make sure to discuss the structural argument of how C++ differs from other programming languages. Don't forget the structural argument.
So the original irritation is half right. Every language ships things and walks them back. What C++ does uniquely is not get to walk them back.
That is the C++-specific part of the pattern, and it is the part working engineers actually feel
Every senior engineer learns this. The juniors find out the first time they reach for
std::listin a code review and the senior gently asks them to usestd::vectorinstead.
The new feature is welcome. The pattern is what you should learn to read.
It is not lost on me that this article is written by HFT University, where "engineers prove how fast their code really is" and can "Earn Bronze, Silver, and Gold badges for your benchmark scores" for your Linkedin profile. That feels pretty scammy, like it's designed to get hopeful undergraduates that need a job to buy their book. And if it is, then the article is doing everything it's supposed to do by getting any amount of discussion or links from Lobsters and HN.
But I still want to call out the AI writing because I want to be able to distinguish actual reason from the appearance of reason. I try to write accurately and with empathy, and I want to read things written the same way. I hope this can help others with that, although I admit some of the motivation is that it's fun to make a really big list of AI quotes, just like it's fun to laugh at a trainwreck like the C++ standard library.
But I wouldn't write this if I thought it didn't matter. And my biggest problem with the article really (except for it probably being a scam) is that it doesn't seem like the author thinks standard library design matters either.
Thank you. I couldn't get through the article at all, even though I think it had something real to say -- I especially cannot stand every other paragraph being this punchy and sexed up. Skilled human writers use emphasis sparingly, and to make a point.
Outside the standard the replacement is Boost.Regex, RE2, or PCRE2. Production code uses one of those. The standard std::regex exists for tutorials.
Ugh. This is so grating.
a person who made this post for a reason
Any shred of respect I might have had left disappeared when I noticed the article had no byline.
Trying to think about whether there's been similar churn in the Rust ecosystem...
I think that's it for really major stuff? Given that std has been stable for 11 years, I think that's pretty remarkable. The rest of std is still there, still works, and 98% of it is still considered idiomatic.
It feels to me like the C++ stdlib is in the dangerous position of being very trigger-happy for feature addition but also being shockingly conservative about deprecating anything, ever, in any circumstance.
The Iterator trait borrowing from its contents comes to mind, which is a perennial problem that comes up in Rust conversations about why something can't use it and requires a workaround.
Likewise f32 and f64 not implementing Cmp and instead having f32::total_cmp as a method, which is an annoyance that comes up very often with new engineers, requires me sighing and explaining the issue.
The panic format machinery is also not that great, and there is a large body of blog posts pointing out that it ends up using a substantial amount of their executable size because the default panic handler uses formatting and is difficult to disable.
I view f32 and f64 not implementing Cmp as a great feature of Rust, since it is such an annoying and problematic source of bugs in other languages. Sure, sometimes one has to use a workaround, but at least it is an explicit choice.
Those are all 100% true, and they're certainly design flaws, but I don't think that anybody would claim that one should avoid them in production Rust code (with a few unusual exceptions like embedded, where binary size really matters), unlike most of the examples in the original post.
the removal of std::thread::scoped (it's since got a replacement that soundly does the same thing)
Do you mean std::thread::scope?
That's the replacement, the original was removed in 2015 in this PR (this links directly to the scoped fn).
My personal opinion is that the outdated design of the Standard Library does a great disservice to the popularity and usability of C++.
Many issues that are often attributed to the language itself should often be redirected to the Standard Library.
E.g. "C++ is slow to compile" is not really true, there's nothing inherently slow in using C++ features. The problem is that the Standard Library is slow to compile due to massive header bloat/dependencies and copious use of templates for even simple abstractions.
Another example: "C++ is unsafe". This is partly true -- C++ is indeed an inherently unsafe language in various aspects, but the design of the Standard Library makes that even worse. There's no reason why the same safer patterns used in Rust's API design couldn't be applied to a newer version of the Standard Library.
Of course, this is all a very complicated issue, as one of the main advantages of C++ is backwards compatibility...
There's no reason why the same safer patterns used in Rust's API design couldn't be applied to a newer version of the Standard Library.
Well, in a few cases, sure. vec[idx] could be written such that it throws or aborts instead of doing out-of-bounds access/UB. But there's also a bunch of cases where the language differences make it much harder to have safe APIs in C++, e.g.
Rust has destructive move by default, C++ doesn't. This forces smart pointer APIs to be either unsafe or at least surprising and crash-y (e.g. by having accesses to a moved-from smart pointer abort the program).
Rust has lifetime annotations, C++ doesn't. So Rust can prevent e.g. iterator invalidation in the design of its iterator API whereas C++ can't really.
Rust has pattern matching, so APIs like Option can offer ergonomic "look and leap" access. C++ could offer a version of std::option that doesn't just UB on empty access, but it would be much more annoying to use than what either C++ or Rust have today. Rust's ? operator also helps a lot here.
edit: and yes I'm aware that you can retrofit something like pattern matching into C++ via overload sets, as std::variant does. I consider it much harder to use and more error-prone though.
I would argue that the same is true for C. A lot of the problems with C is that the stdlib sucks. I'd argue that a modern library with strings and array libraries and even some generic containers and native support for allocators would go a long way in making C more ergonomic and easy to use (of course some flaws of the language cannot be wished away by just replacing it but it does go a long way)
If you look at some modern C code bases and how they extensively use allocators and custom libraries for strings, vectors, hashtables and file system operations, you'd find that they're not really that hard to follow (assuming you have experience with C and/or manual resource management)
How do you make generic containers in C though? Have the user cast between the element type and a void pointer?
I get that it's sometimes the right choice, but it can hardly be called ergonomic in this day and age.
How do you make generic containers in C though? Have the user cast between the element type and a void pointer?
Embed the container's per-element data in the larger structure and use container_of. This is effectively the same as void pointers, but tends to have better ergonomics (due to less indirection and no separate allocation). In practice, type safety is not too big of a deal as most containers can be made local to a single file that provides type-safe accessors. That said, certain structures (such as hash tables) are less performant in the generic case since the hash function must be passed as a pointer.
I like the approach I've describe here: https://raphgl.github.io/blog/generics-in-c.html It's also used by the STB libraries. The nice thing about "module generics" is that you can add all type variants in a single file and only compile it once and not have to pay for every single instantiation (if you so wish).
Lately I've starting calling them something like Vector_of_int or HashMap_String_to_Whatever
It is a bit verbose but it fits really well into the preexisting tools available in the language.
Interesting. If the language could provide syntactic sugar doing what those macros do, I guess it could be calles ergonomic.
Thanks!
at work, we have a slice<T, N> implementation which can express "pointer to exactly N byte" or "pointer to any number of bytes".
it has head(n), tail(n), slice(start, end), index operators, and all of that with bounds checking.
it's such a pleasure to work with this kind of abstraction, but you basically have to port Rust and Zig std libs to C++ i order to get a modernish and safeish language.
but it's worth effort in the end.
I have one of those too. :) But these days, doesn’t std::span do that?
If you remember to use at() instead of [], or turn on your compiler-specific flag, or use gsl::span instead.
Now that I’ve said that, I’m surprised this article didn’t mention gsl.
(Edit: btw, classic C++ism here, where the safe choice is the verbose one, and the unsafe choice is the ergonomic one!)
Not only is the safe alternative more verbose, it came way later. std::span and it's UB-prone operator[] were added in C++ 20, .at() is new in C++ 26.
yeah, but we're basically shipping out own standard lib, as the c++ std lib wasn't suitable for embedded engineering, and our codebase exists since pre-c++11
The problem is that the Standard Library is slow to compile due to massive header bloat/dependencies and copious use of templates for even simple abstractions.
wouldn't you lose performance if you decide to use templating less?
After using C++ for 20 years (and I still do) I must agree a lot with this post. What I really enjoy when writing Rust nowadays is the great standard library and packages ecosystem, more than any memory safety thing.
A striking example is the ranges library, which after 6 years from its standardization is still not fully implemented by the major standard libraries and, even if it was, contains only a handful of combinators where the equivalent in Rust (methods on Iterator) count 76 methods and other 130 are one cargo add command away in the itertools trait.
Another thing I really miss is pattern matching, to make union types (e.g. std::variant) ergonomic.
A proposal is being discussed but is still not in C++26 which is a shame. On the other hand we have contracts and executors which honestly nobody I know asked for.
One of the problems with C++ is that there is no, written, formal, set of criteria for when a feature needs to be a language or standard-library feature. My rules (in general, not specific to C++) are:
A feature belongs in the language if it supports a desirable use case and cannot be expressed in the standard library. If possible, the desired feature should be decomposed into minimal independent things that can be used for other purposes as well.
A feature belongs in the standard library if it is used in almost all codebases. A type belongs in the standard library if it is commonly used as interfaces between libraries. You don't want every library defining its own tuple type or string, for example (C++ did require the former until C++11 and still does for the latter because std::string is a disaster). Note that this applies to interface types as well (which C++ mostly does with concepts these days).
Everything else belongs in reusable modular libraries. Rust is quite good at having a set of stable and blessed external libraries, so there's much less pressure to say, for example 'every game written in Rust needs these data structures so we should put them in the standard library', because people writing games in Rust will just grab the crates with the stuff that they need. C++ has never really embraced the idea of 'good packages that are recommended for this problem that many, but not the majority of, people have'.
Can you expand on std::string being a disaster? Is it about encoding, SSO, the API, ...?
All of the above. It is not really a string, it is a contiguous memory block storing a value type. That value type is typically ‘char (std::stringis actually an alias forstd::basic_string<char>`).
This tightly couples the abstraction (a run of characters) to both the encoding and the data representation. You can get big speedups in any application that does a lot of text processing by changing the representation. Things like tiny strings embedded in pointers, strings that delegate storage elsewhere, strings stored as skip lists over paragraphs in contiguous allocations, and so on will all be faster in some use cases. No interface that takes standard-library strings can use any of these for storage.
The fact that .c_string() returns memory owned by the object also means that implementations must either hold two copies of the data, or always store a null terminator. This effectively precludes a std::string_view-like type being a string.
Encodings are simply not handled. The string says nothing about what the characters mean. A string containing any two 8-bit character types is the same type and has no API for expressing this. The storage and exposed types for character data are also tightly coupled. It’s often useful to special-case Unicode strings where the data happens to be ASCII, because these can be trivially mapped to Unicode characters and to Unicode grapheme clusters. But if you want a Unicode string you need to use the code-point type as the template parameter, there’s no ‘iterators on this expose Unicode code points, internal storage format is none of your business’ API. Low-level string code often wants to deal with Unicode code points, but most higher-level code actually cares about grapheme clusters. There’s no standard way of iterating over grapheme clusters in a standard string.
The only use cases where the particular point in the design space that the C++ standard picked makes sense is where you don’t care about features or performance. There’s a reason big C++ frameworks and libraries all bring their own string abstractions (WebKit had three or four, last time I checked).
What I'm worrying about is which of the stuff that is currently being added will end up being walked back in the end. Contracts just landed in C++ 26 and people are already pointing out severe design flaws.
I'm not a fan of throwing shade on "design by committee" in general, because I do think these organizations serve important purposes and have unique strengths. But those strengths lie not in the "green field" design of entirely new features. Where WG21 (and WG14) really shiny is taking a feature with a somewhat well-explored design space and ideally multiple existing implementations and turning it into a standard feature that all or most of the users and implementors can get on board with. This happened with std::embed for example. But when they standardize something before anybody even pulls off an implementation, like the GC extensions mentioned in the article or std::memory_order_consume or C++ 20 modules, that's when things tend to go really badly.
I'm not a fan of throwing shade on "design by committee" in general
Both the C++ and Haskell languages were designed by committee, but are about as different as two languages could be. I remember this any time I'd be tempted to think that "$X was designed by a committee" implies something about $X.
It was quite a shock to me when I realized a while ago that C++ does not version it's standard library. I didn't expect the article to point that out specifically.
It's intriguing that Go is mentioned as being similarly conservative when it comes to forward-compatibility. Yet somehow, Go managed to avoid most of C++'s issues in this regard simply by being similarly conservative about the added features. I guess not having a stable ABI also helps here.
I think libcamera is the only popular library I know of that explicitly exposes a C++ ABI (which is quite annoying). In my experience, even C++ libraries usually export symbols through a C ABI, since that also makes language interop easier. But maybe I'm just out of the loop.
Also, aren't there some quirks when it comes to ABI compatibility between e.g. Clang and MSVC? I recall Conan explicitly discouraging or disallowing mixing compilers, which makes me wonder why the C++ committee even bothers trying to preserve ABI stability.
It was quite a shock to me when I realized a while ago that C++ does not version it's standard library. I didn't expect the article to point that out specifically.
That's not quite true. It does not version the standard library independently of the language.
But there are two closely related things here: the standard library specification and implementations. The specification is for a complete language-plus-library combination. Implementations aim to provide support for at least one version of the specification and will typically.
I think libcamera is the only popular library I know of that explicitly exposes a C++ ABI (which is quite annoying). In my experience, even C++ libraries usually export symbols through a C ABI, since that also makes language interop easier.
There are a lot of libraries that expose C++ interfaces, including some very large ones like Qt.
The problem is that the C++ abstract machine does not define anything about the linkage process. This means that it cannot define things about how dynamic libraries work. Dynamic linkage for C++ on UNIX systems follows the C model: pretend it's dynamic linkage and make it the loader's problem. This leads to a bunch of horrific things like copy relocations. Windows has a much more principled notion of what a shared library is, but this means that some idioms for UNIX C++ libraries can't work on Windows.
Shared libraries have problems for features like C++ templates (or generics in other languages if they support compile-time reification). If you want to be able to instantiate a template with a user type, the entire definition needs to be in a header in C++ because the compiler can't see across compilation-unit boundaries. For a shared library, this means that you end up having the same code instantiated in multiple places. If a program and a library both instantiate the same template with the same parameters, they will both have copies of it and the linker and loader need to make sure that only one is used in the final loaded program.
Contrast this with Swift, which explicitly says 'shared libraries are a thing, we will expose language-level constructs for expressing them'. If you want to expose a generic across a shared-library boundary, you can but it will be explicitly lowered to a dynamic-dispatch version for all external callers. The C++ equivalent of this is possible to implement by hand: you create a generic version of your template that uses a type-erased wrapper (using virtual functions, inheriting from an internal class) and use other concrete instantiations explicitly. But this is hard and manual, whereas in Swift it's just 'this is what happens at a shared-library boundary'.
The same thing with hiding types. C++ has the pImpl (pointer to implementation) pattern for creating public interfaces that expose the behaviour, but not implementation, across a library boundary. Swift just has an abstract machine that knows where a library boundary is and says 'the size of types not explicitly marked as ABI-stable is not a compile-time constant across a shared-library boundary'.
The other form or reality denying in the standard: almost every non-trivial C++ codebase I've worked has been compiled with -fno-rtti -fno-exceptions, or their CL.EXE spelling of the same. The standard does not accept this as a possibility. Most standard-library functions still expect to use exceptions for error reporting and so will just call abort if compiled with -fno-exception. This makes anything in the standard library that does dynamic memory allocation unusable on embedded: std::vector<T>::push_back can cause your program to crash.
From the article:
The committee is not only failing to remove bad features. It is also continuously adding new ones that no working engineer asked for, championed by individuals who get professional recognition for shipping the proposal, and the result is a language whose surface area expands faster than any single team of implementers can keep up with.
This is 100% how we got contracts. Verus shows what a good system using contracts can enable in a language targeting the same kinds of environments as C++. The P2900 contracts are a combination of conflicting requirements that make every problem that contracts might be a fit for worse.
The result is a labour market in which "C++ engineer" is a much higher-paid job than "engineer who can program", because the C++ part requires a museum docent's familiarity with fifteen years of accumulated walk-backs and Vasa features
I don't think that's true, because nowhere actually writes to the C++ standard, they write to their own favourite in-house subset of a superset.
If you want to expose a generic across a shared-library boundary, you can but it will be explicitly lowered to a dynamic-dispatch version for all external callers.
It's good that this is the default. For anyone reading this and assuming it might cause performance problems: nah you can turn it off explicitly where you need to, with annotations like @frozen.
How does that work? Is there some kind of IR shipped along with the shared library that the Swift compiler can read?
This blog post may hold the answer: https://faultlore.com/blah/swift-abi/
I'd answer you directly if I remembered :(
There's a good talk about it here: https://www.youtube.com/watch?v=ctS8FzqcRug (slides: https://llvm.org/devmtg/2017-10/slides/Pestov-McCall-ImplementingGenerics.pdf)
Basically, when inside the shared library boundary, the compiler can monomorphize the generics as usual, but across that boundary it generates an equivalent, type-erased version of the function that uses what they call witness tables containing RTTI, so something like
func getSecond<T>(_ pair: Pair<T>) -> T {
return pair.second
}
compiles into
void getSecond(opaque *result, opaque *pair, type *T) {
type *PairOfT = get_generic_metadata(&Pair_pattern, T);
const opaque *second =
(pair + PairOfT->fields[1]);
T->vwt->copy_init(result, second, T);
PairOfT->vwt->destroy(pair, PairOfT);
}
That’s the bit I posted about originally. My question is how it does monomorphisation across a shared p-library boundary, for things using the frozen attribute, without access to the source code for that library.
Don't know, I assume that's it.
I've heard the ABIs for some of the most performance critical widely used types like arrays are frozen and lots of their methods inlineable.
Another thing that is helpful for performance btw is that resilience only comes into play for exposed APIs. Code that is internal to a library gets compiled with full knowledge of its types' layout, no indirection necessary. So if you design a library that does a lot of work with only a handful of calls into its API, you'll get negligible overhead from resilience.
Gankra's writing (faultlore.com, which aapoalas linked in the other comment) is really good.
It's intriguing that Go is mentioned as being similarly conservative when it comes to forward-compatibility. Yet somehow, Go managed to avoid most of C++'s issues in this regard simply by being similarly conservative about the added features. I guess not having a stable ABI also helps here.
go vet is a valuable tool here too, providing automated upgrades for API improvements.
Mods (@pushcx, @irene, @355E3B, @aleph), I feel that this article is over the line. The tone is one of pure contempt throughout; AND its only credited author is a company; AND it is unedited LLM output that wastes far more of the reader's time than the content warrants.[1]
If this were respectful, or personal, or effortful, that would be better. But companies should not benefit from computer-assisted controversy-farming.
[1] I have more objections against LLMs. I picked one that I think we can all agree on.
OP here. I'll try to be concise.
P.s. Is this the right way to modcall? Or is just the word 'mods' enough? Or is a DM preferred?
Go is the most conservative. The Go 1 compatibility document is explicit: "programs written to the Go 1 specification will continue to compile and run correctly, unchanged, over the lifetime of that specification." Even there,
io/ioutilwas deprecated in Go 1.16 and the functions redirected toioandos, but the package still exists because the promise forbids actual removal. Go's design choice is to live with the mistakes rather than ship the breakage.
I was thinking that this is false because I remembered the backward-incompatible change to for loop semantics: https://go.dev/blog/loopvar-preview. But it turns out Go uses something similar to Rust's editions here, the semantics only change if you declare a Go version of 1.22 or newer. You could probably also remove io/ioutil like this. But I guess it's just not worth to break code over this, even across an edition boundary.
Probably no one ever wrote code that relied on the old behavior there... that'd be nuts.
So if the pattern was there, chances are it was buggy already and if it wasn't it was very much too cute.
I’ve pretty much abandoned C++ since last year (first for Kotlin, then for Swift.) I still have to maintain some C++ at work, but the new code I’m writing is so much cleaner, concise-er and safer. I’m making tradeoffs in code size and maybe performance, but it’s worth it.
Rust probably could not exist in its current form if C++ didn't actually try out all these bad ideas in practice and prove them as such. Big Thank You!
I'd be interested in a Rust-like standard library replacement for C++. I know of rpp, which aims to do just about that: https://github.com/TheNumbat/rpp
Are there other options? Rather than other implementations of the C++ stdlib, like EASTL, I am talking about libs which more closely follow Rust. I know that bits like std::initializer_list are baked into the syntax, but everything else can go.
The Rust standard library is possible because the Rust compiler has a borrow checker. Most Rust idioms are quite error-prone when expressed in C++. If you use an interior pointer and mutate the owner, the Rust compiler will complain, a C++ compiler will just let you do the thing and potentially trigger undefined behaviour. If you move a value from a variable and then use the variable, the Rust compiler will shout at you, a C++ compiler will simply let you operate on an object in a valid but unspecified state.
Aside from the LLM stuff, this article is just confused with itself: a repeating complaint is that C++ can't remove bad parts, while at the same time highlighting the parts that C++ has removed. Also comparisons to Rust's std which really cannot remove anything except in very exceptional cases (unsoundness).
If I had to blame C++ for something, it'd not be a lack of change, nor a lack of walk-backs, nor even design-by-committee. It'd simply be that it's a very complicated language where not making mistakes is hard.
Designing and maintaining large systems is hard.
You design something as best as you can, and avoid the most obvious mistakes, and then you deliver it. But you don't know whether there are more subtle mistakes until the thing has been out there for years, with lots of usage.
The alternative is never to deliver anything, and to keep tweaking stuff and fixing mistakes as you get more experience with it. You can do this for years and never deliver anything. The industry is littered with projects like this. Sun's Distributed Objects Everywhere and the Taligent/Pink project are examples that come to mind. I'm sure there are others.
Once you've shipped something, inevitably mistakes will be discovered. What can you do about it now? Well, you can fix them. But that breaks all your users. This results in a better system, but with fewer users. If you do this often enough, you alienate your user base. An example of this might be Scala; I seem to recall a stretch where Scala made incompatible changes three releases in a row. I think they lost a lot of users that way. Maybe somebody who knows more about Scala can comment on what effect this had.
OK now so if you don't fix the mistakes, you're forced to live with them if you want to preserve compatibility. I'm a Java guy, and in fact as "Dr Deprecator" I'm responsible for deprecation in the Java platform, so it was gratifying to see the article extol Java's virtues in deprecating and actually getting rid of stuff. Great. What I also see though is all the other stuff in Java that has errors that we haven't fixed yet and indeed that we have decided to keep in the platform.
For example, there's java.util.LinkedList. It's fairly analogous to C++'s std::list, and it has all the same problems. The advice to Java developers is "don't use LinkedList." It's still in there. Why? Despite this advice, there are lots of old programs out there that use it. Maybe these programs would be somewhat smaller or faster if they used something else. However, their use of LinkedList is probably just fine. It's not worth the effort to update these legacy programs to gain some marginal benefit by migrating away from LinkedList. From the perspective of the Java platform, we view it as a service to developers that their old applications keep running on newer versions of Java. That's why we keep old stuff like LinkedList in the platform.
I don't think Java is particularly unique in this regard. Every platform has its receipts.