Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm glad for this article.

C++ has been my main language for a very long time, but I've been a grumpy skeptic of C++ since around C++14 due to the language spec's total complexity. So I've mostly stuck with C++11.

But now that C++ has modules, concepts, etc., I'm starting to wonder if C++23 is worthwhile for new projects. I.e., the language-spec complexity is still there, but the new features might tip the balance.

I'd been thinking to walk away from C++ in favor of Rust for new projects. But now I might give C++23 a chance to prove itself.



C++ deserves its rep for complexity. But, it comes from a promise to avoid a version upgrade debacle in the style of Python 3. C++ promises that you will forever be able to interleave new-style code right into the middle of ancient, battle-tested old-style code.

To do that, it can only add features and never take them away. Instead, it adds features that deprecate the practice of PITA patterns that were common, necessary and difficult.

Like, SFINAE was necessary all over the place to make libraries "just work" the way users would expect. But, it is a PITA to write and and PITA to read. Now, constexpr if and auto return types can usually collapse all that scattered, implicit, templated pattern matching down to a few if statements. Adding those features technically made the standard more complicated. But, it made new code moving forward much simpler to understand.

Similarly: Before variadic templates, parameter packs and fold expressions, you had the hell of recursive templates. Auto lambdas make a lot of 1-off templates blend right into the middle of regular code. Deduction guides set up library writers to set you up to write

    std::array names{"Alice", "Bob", "Charlie};
instead of

    std::array<const char*, 2> names{"Alice", "Bob", "Charlie};


> But, it comes from a promise to avoid a version upgrade debacle in the style of Python 3.

There is a very wide middle ground between C++'s "Your horrific unsafe code from the 80s still compiles" and Python's "We changed the integer values common operations on strings return at runtime with absolutely no way to statically tell how to migrate the code".

In Dart, we moved to a sound static type system in 2.0, moved to non-nullable types in 2.13 (sound and defaulting to non-nullable!), and removed support for the pre-null safety type system in 3.0. We brought almost the entire ecosystem with us.

Granted, our userbase is much smaller than C++'s and the average age of a given Dart codebase is much younger.

But you can deprecate and remove old features without causing a decade of misery like Python did. You just need good language support for knowing which version of the language a given file is targeting, good static typing support, and good automated migration tooling. None of those is rocket science.


> But you can deprecate and remove old features without causing a decade of misery like Python did. You just need good language support for knowing which version of the language a given file is targeting, good static typing support, and good automated migration tooling. None of those is rocket science.

That's way easier to do in a naturally statically-typed language though.


Yes, which C++ is.


Except there is only one implementation, with language design and implementation developed together.

C++, Java and .NET are now all living through "decade of misery like Python did", exactly because not everyone is jumping into the latest versions of the languages and ecosystem changes.


Isn't this a problem only if you manage your dependencies by source rather than "compiled"?

That is, in Java you may not be able to compile old code with new javac, but the class file format is still understood by the JVM. So your old .jar still works.

I believe it is the same in C++ with the .so. I don't know about .NET.

In Python however, I don't think the .pyc were compatible between Python2 and Python3.


Your jar file only works if what it expects to find it is still there, it will crash and burn otherwise.

Same applies to other examples.


I'm not sure to understand.

You mean if your jar depends on other stuff?


Of course.

The way the bytecode is executed, the standard library, possible changes in GC and JIT implementation, other 3rd party libraries.

For example, if the jar code has a call to Thread.stop() and is loaded in Java 11 or later, when it comes to actually call that method you will get a java.lang.NoSuchMethodError exception.


Got it, thanks.


> To do that, it can only add features and never take them away. Instead, it adds features that deprecate the practice of PITA patterns that were common, necessary and difficult.

The result being that programmers have to learn every single one of those ways of doing things in order to read your coworkers code. Give me python 3 breaking changes any day


> C++ promises that you will forever be able to interleave new-style code right into the middle of ancient, battle-tested old-style code.

this is a problem, btw AFAIK c++ modernizations are few and rarely successful.


When I learned C++, there was no auto. Range-based for loops didn't exist. The combination of those two were particularly nice because instead of writing:

  for(std::vector<sometype<int>>::const_iterator it = my_container.cbegin(); it != my_container.cend(); ++it){
    // use *it
  }
You could use:

  for(auto const& val : my_container) {
    // use val
  }
Which really paved the way for less hesitation around nesting templates and other awkward things.

Same goes for map with the introduction of structured bindings:

  for(std::map<somekey, sometype<int>>::const_iterator it = my_map.cbegin(); it != my_map.cend(); ++it){
    // use it->first, it->second
  }
became

  for(auto const& [key, val] : my_map) {
    // use key, val
  }
The introduction of std::variant made state a lot easier to reason about. Unions meant too much room for error, and cramming polymorphism into places it shouldn't have ever been was riddled with its own performance and memory issues.

std::optional was also a game changer in that regard.

constexpr was huge. It killed off a bunch of awkward template metaprogramming for something faster to compile and easier to deal with. I wasn't a part of the consteval discussions but from what I HEARD people say about it and what it actually WAS, I think it was castrated at some point along the way.

Concepts, also a big one.

Formatting (RIP iostreams) is much nicer than the alternative.

Ranges, coroutines, modules - they'll have their day too.

And we have a bunch of stuff to look forward to. Senders and receivers solves a lot of component interaction problems. Reflection will rid us of so much annoying provisioning of classes. Pattern matching and expression statements will be major usability boosts.

Ok, C++ doesn't move at the speed of newer languages, but for its size, complexity and all of the hurdles that it has on the way to standardisation, I think modernisations are many and often successful.

I would absolutely love a major break that fixed things we can't currently fix due to ABI risks, implementer resistance, etc. I'd love to rid us of the old ways™ and focus on safety the right way™ - by removing bad bits rather than just adding good bits and telling people to use them and follow a bunch of guidelines. I'd love better defaults.

There is so much work to do to make C++ great again puts on MCPPGA hat, but I can't agree that modernisation are few and rarely successful.


Simply listing the features of C++ makes it sound good until you actually try to use them and realize they don't quite compose and have little holes here and there exactly because its actually 3 languages from 3 different era trying to mix together.

For example, we have std::variant, yay! So where's the pattern matching? Vomits from a glance


Most serious C++ developers will tell you that C++14 was basically a bug fix for some oversight in the C++11 spec. You should probably use it if you can if that’s the standard you’re happy with.


C++ 17 is the sweet spot for me. It has most of C++ 11/14, with many new features I use a ton: constexpr, <charconv>, <string_view>, [[maybe_unused]], new SFINAE feature (if constexpr), and more. (And this was considered a small release!?)

I guess I’m just right at home and therefore a bit reluctant to jump into C++20. That and the constantly changing “””correct way of doing things.”””


If you use SFINAE that alone is reason to move to C++20, to get concepts instead.


I get the same feeling regarding many features on C#, F#, Java, Python, Typescript, Scala, Kotlin, Swift.

Don't forget it isn't only the language, the various implementations, standard library, breaking changes, and major 3rd party libraries.

So I embrace complexity, as the alternative is being stuck with something like Go.


Really the cognitive load of modern Rust is no less than C++$RECENT in my experience. Both require a ton of prima-facie concepts before you can productively read code from other developers. Of en vogue languages, Zig is the only one that seems to view keeping the "metaphor flood" under control as a design goal, we'll see how things evolve.

But really, and I say this from the perspective of someone in the embedded world who still does most of his serious work in C and cares about code generation and linkage: I think the whole concept of these Extremely Heavy Systems Programming Languages is not long for this world. In modern systems, managed runtimes a-la Go/Swift/JVM/.NET and glue via crufty type-light environments like Python and Javascript are absolutely where the world has ended up.

And I increasingly get the feeling that those of us left arguing over which monstrosity to use in our shrinking domain are... kinda becoming the joke.


> Really the cognitive load of modern Rust is no less than C++$RECENT in my experience. Both require a ton of prima-facie concepts before you can productively read code from other developers.

Eh. I don't know if I agree. I've worked on a few large C++ codebases, and the cognitive load between Rust and C++ is incomparable.

The ownership/borrowing stuff is complexity you deal with implicitly in other systems languages, here's it's just more explicit and semi-automated most of the time by the compiler.

In C++ the terse thing is never the correct thing. If I'm using metaphors: a Rust sentence, in C++ usually has to be expressed through one or more paragraphs. The type system is so loose you have to do "mental" expansions half the time (or run the build and pray for no errors, or at the very least that the error is somewhat comprehensible[1]).

There's some low-level stuff that can be ugly (for various definitions of ugly), but that's every language. The low level bits of async are a bit wired, but once the concepts "click" it becomes fairly intuitive.

At least the ugly parts are cordoned behind library code for the most part, and rarely leak out.

I guess it could just boil down to familiarity, but it took me much less time to familiarise myself with Rust than it took me to familiarise myself with C++. We're talking months vs years to consider myself comfortable/adept at it. Although, maybe just some C++ or general programming wisdom transferred over?

[1]: This happens in Rust too, I must admit. But it's usually an odd situation that I've encountered once or twice with some very exotic types. In C++ it's the norm, and usually also reported with bizarre provenance


C++ errors are so bizarre that most of the time I use the compilation result as a simple binary value. Some part of one's mental neural network learns how to locate many errors simply through a "it doesn't work" signal.


IMO the cognitive load of Rust is different in practice just from basic nuts and bolts things like having a ecosystem of libraries that culturally emphasizes things like portability and heavy testing, and a standard package manager and an existent module system (C++26 fingers crossed). I dislike Cargo but it's incredibly effective in the sense any Rust programmer can pick up another project and almost all of of the tools instantly work with no change. I mean, Rust is frankly most popular in the same space Go/Swift et cetera are, services and application layer programming, where those conveniences aren't taken for granted. I see it used way more for web services and desktop/application services/command line/middleware libraries than I do for, like, device drivers or embedded kernels. Those are limited enough in number anyway.

Really, the ergonomics of both the language features and standard tooling meant it always meant it was going to appeal to people and go places C++ would not, even if they in theory are both "systems languages" with large surface areas, and they overlap at this extreme end (kernels/embedded/firmware/etc) of the spectrum that little else fits into.


I don't write embedded software, but I see Rust breaking into many domains outside of systems and winning developer mind share everywhere. Seemingly fulfilling its promise of being able to target low-level and higher-level software domains (something Chris Lattner wanted for Swift, but it hasn't yet materialised on the low-level side, we'll see).


I'm in doubt of this statement, Rust is here for 17 years, its market share is less than 1% still(per google).

It does have a lot of developers saying good words for it whenever there is a chance in recent few years, but the reality is that, it's hard for most developers to learn, because of that it might remain to be a niche language for system programming.

c++ is not standing still, I feel since c++20 it becomes a really interesting modern language, and I hope its memory safety concern will be addressed over time at a faster pace, in fact if you use rule-of-zero, RAII, smart pointers etc correctly your code can be very much memory-safe already.


> It does have a lot of developers saying good words for it whenever there is a chance in recent few years, but the reality is that, it's hard for most developers to learn, because of that it might remain to be a niche language for system programming.

On what basis is this "reality" based? Recent survey of 1000 developers by Google denotes "[the] ramp-up numbers are in line with the time we’ve seen for developers to adopt other languages"[1]

> in fact if you use rule-of-zero, RAII, smart pointers etc correctly your code can be very much memory-safe already.

That's not what memory safe means. Memory safe means that even if you make a mistake, you cannot get a memory error, something that is true of Rust minus the `unsafe` superset. Even with what you describe, you can trivially get UB on C++ if you: keep a reference to an object, use a smart pointer after move (something that the compiler will not warn against), call .front on an empty vector, dereference an iterator that got invalidated by pushing in your vector, use a non thread-safe object in a multithreaded context. Rust will statically prevent a memory error from occurring in any of these situations.

If you're cautious, you may not get memory errors in a C++, but that's not "memory safe".

Also, as a C++ developer turned Rust developer, C++20 does nothing to capture my interest back. The module system is atrocious (orthogonality between modules and namespace, way too complex for what it wants to achieve, module partitions seriously?, poor tool support 3 years after the standard was published), there are no ergonomic sum types and pattern matching, no standard package manager (and the legacy compilation model based on textual inclusion makes build system more complex than they could be), no improvements on thread safety in sight (to be fair, you pretty much need your language to feature a borrow checker and be built around thread safety, it cannot be bolted on). I have little hope C++ is in capacity of addressing these issues in the next 6 years.

[1]: https://opensource.googleblog.com/2023/06/rust-fact-vs-ficti...


According to Wikipedia, Rust appeared for the first time on May 15, 2015; 8 years ago.


That was the release date of Rust 1.0. Not the first appearance.


Rust was a very different language pre 1.0 (GC, builtin green threading, etc). I think it's fair to start counting at 1.0.


Actually if you look back to 8 and 16 bit home computers, with C, C++, Object Pascal, BASIC, Modula-2, full of inline Assembly, in many cases that being the biggest code surface, it is quite similar.

Nowadays C and C++ took the role of that inline Assembly, with higher level systems languages taking over the role of the former.


Sure. The point was more that Big System Engineering (in any particular domain, frankly including things like OS kernels), going forward, just won't be happening much in Rust or C++. And languages that large make for very poor "inline assembly", where C obviously does quite well and Zig seems to have a reasonable shot.

I mean, literally yesterday I had to stop and write a "device driver" (an MMIO firmware loader for an on board microcontroller) in Python by mapping /dev/mem. That's the kind of interface and development metaphor we'll be seeing in the longer term: kernel driver and interface responsibilities will shrink (they are already) in favor of component design done in pedestrian languages like Go or whatever that mere mortals will understand. There's no market for a big C++ or Rust kernel framework.


I think the future of these languages is largely as a target for code generation and transformation. Their legacy tooling-unfriendly designs is what's slowing this transition.


>I'd been thinking to walk away from C++ in favor of Rust for new projects. But now I might give C++23 a chance to prove itself.

Modules were a great step forward for C++, but one of the features I enjoy the most about Rust is that adding a library just works. Especially for single person projects, which are on the smaller side, it feels absolutely great having to do near zero build management.


It's either the complex language or the complex code.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: