C++ code bases are really a lot longer-lived than any other software builds upon them. Hence we cannot drop it.
Starting with C++17, I think committee has been doing the language a disservice and piled even more and more unintended complexity by rushing "improvements" like modules.
I don't write C++ anymore due to my team switching to Rust and C only (for really old stuff and ABI). I am really scared of having to return though. Not because I am spoiled by Rust (I am though), but because catching up with all the silly things they added on top and how they interact with earlier stuff like iterators. C++ is a perfect language for unnecessary pitfalls. Newer standards just exploded this complexity.
https://en.cppreference.com/w/cpp/locale/codecvt_utf8.html
https://en.cppreference.com/w/cpp/algorithm/random_shuffle.h...
Mozilla and Dropbox did it. LLMs are good at translating between languages, and writing unit tests to make sure things still work the same.
12%. Assume the progress is linear (not logarithmic like most cases), we just need 60 more years to migrate those c/c++ code.
https://www.phoronix.com/news/Google-Linux-Binder-In-Rust
https://arxiv.org/abs/2503.23791v1
https://www.darpa.mil/research/programs/translating-all-c-to...
https://link.springer.com/content/pdf/10.1007/s10664-024-105...
> Everybody would be doing it by now
Models and agents have progressed significantly in the last few months. Migrating projects to rust can definitely be a thing in the coming years if there is sufficient motivation. But oftentimes c/c++ devs have aversions to the rust language itself, so the biggest challenge can be an issue of motivation in general.
Rust just doesn't have close to the same type of adoption/support yet, especially when considering various embedded platforms.
C++ as C with classes is a pretty good language!
And for the safe parts, the posts that I've read from people who have spent a non-trivial amount of effort with the language do not paint a clear picture either on whether there was really a benefit to the language overall.
So to say that "the world has moved on" in light of all of this is pure hubris.
- GCC switched from C to C++
- CUDA switched from C to C++
But I can understand the decision, and at that time , C++ frontend features and libs were a little bit less horrible.
Please explain in detail how alternatives would have worked better for GCC and CUDA. Also, if you could share some historical context about how those alternatives could realistically have been implemented at the time, that would be helpful too.
I love to hear all the "would've" and "should've" scenarios.
The C++ frontend of MSVC handles (the common) compiler-specific language extensions differently than the other compilers. Besides, its pre-processor behaves differently too. It is now good that there is a clang frontend for MSVC.
If you think that "the world seems to have moved on to Rust", I would recommend to look at the job listings. For example, devjobs.de: Rust: 61, C++: 1546. That's 1:25. Maybe in other countries there are more Rust jobs?
https://play.rust-lang.org/?version=nightly&mode=debug&editi...
These are all highly non-parallel problems. They don't gain much from being parallel, and because Rust imposes 'restrict' semantics on even single threaded code you end up making it much harder to write code in these domains.
This has been my experience with Rust. Shared mutability is safe on a single thread without 'restrict' on all your pointers, and Rust has limited options to opt into shared mutability (with lots of ergonomic caveats).
Don't get me wrong though, I still think Rust is a great tool. It just has tradeoffs.
The idiomatic Rust equivalent of a C non-restrict pointer is arguably &Cell<T>. The biggest problem with it is library code that takes &mut T when the likes of &Cell<T> might suffice (because potential aliasing does not affect the semantics of what that Rust code is doing), but this is an acknowledged problem and the Rust project will take pull req's that fix it where it occurs.
If you're sure you're never going to need multi-threaded environment, you have an option as well: Replace std::sync with std::rc, Mutex with RefCell in the above toy example and that's about it.
If you want to use some asynchronous runtime, replace std::sync with tokio::sync (or std::rc), slap async/awaits along with a single-threaded runtime and that's about it.
Of course, the code above is just a toy example and business logic is much more complex in real world, but compare this to what it would take to write same C++ logic in async.
I found Rust's approach massively more ergonomic compared to C++ approach of passing closures around for asio-like IO contexts or coroutine compiler-magic which opens new novel avenues to shoot myself on the foot, well, to the extent I could grasp it.
It's true Rust forces you to pay all this cost ahead of time. It's also true most applications don't require this level of safety really, so it becomes ridiculous to pay it upfront. And even for some that require such high level of safety, you can skip bunch of bolts on a plane door and it will still be a billion dollar company at the end of the day, so...
The C++ solution would be to start the threads and use an MPSC queue (which, ironically, Rust also has) in order to update the UI.
Rust will eventually stumble upon ergonomics and allow portions of code to be specified as single threaded or embarrassingly parallel, but unfortunately the community evolved horse blinders early on and isn't letting them go any time soon.
You’re forced into function-call or jump-table dispatch, which tends to be slower.
On the other hand, there's the recently-added-to-nightly `become` for guaranteed tail calls, which might work better than computed gotos if CPython is a good example [0]
> When using both attributes [[[clang::musttail]] and preserve_none], Jin's new tail-call-based interpreter inherits the nice performance benefits of the computed-goto-based version, while also making it easier for the compiler to figure out optimal register allocations and other local optimizations.
To be fair I don't think Rust has a preserve_none equivalent yet, but given naked functions are a thing in Rust I'd hope it isn't too bad to support?
And give Rust one or two more decades and it will be the same messy kitchen sink language as C++. If anything, Rust is speedrunning C++ history (with the notable exception of fixing static memory safety of course).
Internet hype meets actual industry reality :-).
I'm thinking the same about your comment :D
There are things you can add, but the rot still permeates the foundations, and much of the newness goes partially unused because they're just not at home in C++. Use of `std::optional` and `std::variant` is, as far as I know, still limited, even in newer C++ code, because the ergonomics just aren't there.
variant isn't, yet. We'll eventually get some kind of structural pattern matching that will make variant or it's successor more idiomatic.
C++ does have quite a bit of rot, you're right. But that's the price of building technology people actually use.
Carbon doesn't seem to have any fundamentally new ideas, we'll see how well it fares in the wild.
Thats the fantastic thing about c++, you can already write an easy to use match, but they just chose not to include that in the stdlib but rather want you write that yourself.
Example:
match(foo,
[](Foo&) {
},
[&](Bar& bar) {
},
[&](const auto& mode) {
// catch all
});
Also optional and expected are ergonomic nightmares since there is no "try!" macro like rust has. In general it lacks the infrastructure that is needed to make these types nice to use. It also clashes with other concepts like RAII where you kinda have to use exceptions when it comes to ctors that may fail.For example, if you had to match patterns to distinguish between these three possibilities when looking at a tree: (val), (val (tree left) (tree right)), (val (tree subtree)). Here, the possibilities are not really separate types, but rather different ways of constructing a type. This sort of pattern shows up in general purpose programming (even meat and potato imperative programming) pretty often.
There was a proposal for this championed by Stroustrup over ten years ago, but it went nowhere IIRC. https://www.stroustrup.com/pattern-matching-November-2014.pd...
I have some hope that the upcoming compile time reflection will make it easier to implement said syntactic sugar.
So I'm afraid no by definition C++ can't adopt Rust's ideas because Rust's ideas were originally impossible C++ ideas.
I agree with your point except for the 'never' qualifier. It was certainly true when Rust was born.
C++ has proven the 'never' part wrong multiple times. I think, by 2030, the only thing that C++ would lack that Rust has right now is the unified toolchain/packaging ecosystem because people are not going to settle that debate.
Everything else is well on its way to be implemented, in one of three forms - core language features (eg: concepts), language features that primarily enable writing more powerful libraries so that you do not have to come up with language features for everything (eg: reflection), and finally tooling support from the compiler as a test bed of what the language could guarantee (lifetime checks and annotations in clang).
Of course Rust is innovating pretty well too, I am very interested in seeing what async/coroutines are going to look like in a few years.
You are free to design your library in a way that your users only see one of these.
C++ didn't get that. The proposal paper at the time says it's impossible (for C++). But what they did propose was the feature you've seen in C++ today, which they call "move", but it has slightly odd (though convenient to implement, Worse Is Better after all) behaviour.
Now you can make this C++ "move" behaviour out of destructive move, that behaviour is roughly what Rust calls std::mem::take and it's sometimes useful, which is why that function is provided. But, often it's not really what you wanted, and if you actually wanted destructive move but only have this C++ imposter then you need to perform the entire take, then throw away the newly created object. You will find lots of C++ code doing exactly that.
So, no, you can't "design your library" to deliver the desirable property in C++. It's just another of the dozens of nagging pains because of design mistakes C++ won't fix.
Yes, I understand that there’s a lot of bad code out there and C++ happily enables that. But that was not my point.
pub fn drop<T>(_x: T) {}template <typename T> void drop(std::unique_ptr<T> &&) {}
Also, you don’t really need this because of RAII. You can make it simpler, and here’s how you’d do this in C++26.
template <std::movable T> void drop(T &&) {}
It can get even simpler!
void drop(std::movable auto){}
Do you see my point about C++ incorporating the good ideas at a glacial pace?
So what happens for objects that aren't behind a unique_ptr?
> Also, you don’t really need this because of RAII.
drop() indeed isn't used much because of RAII, but it is handy for those instances where you do actually want to dispose of something "early".
> dispose of something "early".
This is a bit of an anti pattern in C++, but doable of course.
This is useful in the grpc library I use, which has a response type with some metadata about the response, and an “inner” value that represents the thing the grpc method returned. If you just want the inner thing and don’t care about the metadata, there’s a `fn into_inner(self)` that destroys the response and leaves you the inner value.
You can write similar methods in C++ but they require leaving the original value in an “empty” state so that you can avoid expensive copying of things while still letting the moved-from value be “safe” to use. But that affects your API design… you now have to make the methods that fetch the “inner” thing be nullable/optional so that you can represent “oh this was moved from so there’s no body any more”. You don’t have to do that in Rust: moves are just a memcpy and the compiler will reject programs that use the moved-from value.
Yes, this is something the C++ 'language' is not going to specify other than claiming that it is undefined behavior.
Doesn't prevent compilers from doing it though, clang will happily do this for you right now in most cases.
https://discourse.llvm.org/t/rfc-intra-procedural-lifetime-a...
The RFC you posted has nothing to do with move semantics, it's about references outliving what they point to (ie. use-after-free, etc) and similar to Rust's borrow checker.
But here's the thing: move semantics and the borrow checker have nothing to do with each other! The borrow checker ensures that borrowed data (ie. &Foo, equivalent to C++'s references) is sound, it's not the part that enforces move semantics. That happens earlier in the compilation, the compiler enforces moves well before the borrow checker phase.
I don't think those work either. Not only do neither of those actually end the lifetime of what's passed in, but they have other flaws as well.
> template <std::movable T> void drop(T &&) {}
This literally does nothing. Reference parameters don't affect what's passed in on their own - you need at least something on the other end (e.g., a move constructor) to do anything.
For example, consider how this would be instantiated for std::vector<int>:
template <> void drop(std::vector<int>&&) {}
> void drop(std::movable auto){}I believe this is equivalent to passing by value, so this will actually invoke a copy if what's passed in isn't eligible for a move (or if the move constructor is equivalent to the copy constructor, since copyable subsumes movable). Again, consider how this would be instantiated for std::vector<int>:
template <> void drop(std::vector<int>) {}
> > dispose of something "early".> This is a bit of an anti pattern in C++, but doable of course.
I don't think you can do quite the same thing in C++ without introducing new scopes.
>For example, consider how this would be instantiated for std::vector<int>:
Great, here you go.
https://godbolt.org/z/9v66n6Ta4
And yes, the compiler will not prevent you from using this value. (clang will eventually, I think)
But clang static analyzer will happily detect it.
https://stackoverflow.com/questions/72532377/g-detect-use-af...
We're talking about how the rust compiler uses move semantics to prevent you from using the moved-from value, such that code like this will not compile:
let f = Foo::new();
drop(f);
f.foo(); // Error: use of moved value
C++'s move semantics do not prevent you from using f after you've moved it. On the contrary, it's intentionally allowed. It's not undefined behavior either, it's "unspecified" behavior, which means "behavior, for a well-formed program construct and correct data, that depends on the implementation". This simply means that it's up to the individual type to decide what happens when a value is moved from. (A string becomes an empty string, for instance, or a vector becomes and empty vector.)Rust's move semantics mean:
- You don't write a move constructor
- Moves are just memcpy
- The compiler enforces the old value can't be used any more
C++'s move semantics mean:
- You must write a move constructor (rvalue reference constructor)
- Moves are arbitrary code
- The language explicitly allows the moved-from value to continue to be used
That there are certain linter-style tools like clang-tidy which can be configured to warn on using moved-from values is irrelevant: The standard explicitly allows it. It's personal preference whether you should make a habit of using moved-from values in your codebase, which is why this will only ever be a linter thing. The C++ standard would have to completely change its mind and retcon moves to mean something different, if they ever wanted to change this.
Now, the beginning of this thread was someone saying "Rust is basically the wanted fixes to C++ that C++ itself could never adopt for legacy reasons". Then you came back with "I agree with your point except for the 'never' qualifier", implying C++ will eventually support Rust's ideas. But move semantics in C++ are precisely the opposite of those in Rust, because rust-style semantics were deemed impossible to implement in C++, even though it's what people actually wanted at the time. So I think it's fair to say C++ will "never" get Rust-style move semantics.
As I pointed out, your drop_new is broken for copyable types. For example, consider std::array:
auto w = std::array<int, 3>{0, 1, 2};
drop_new(std::move(w));
std::cerr << w[0] << ", " << w[1] << ", " << w[2] << '\n';
This prints "0, 1, 2". Rust's drop() doesn't suffer from this flaw.> And yes, the compiler will not prevent you from using this value.
Yes, that is the point!
This bit:
std::vector<int> vec {1, 2, 3};
drop_new(std::move(vec));
std::cerr << vec.size() << " <- ?\n";
Simply does not compile in Rust [0]: let vec = vec![1, 2, 3];
drop(vec);
println!("{0} <- ?", vec.len()); // error[E0382]: borrow of moved value: `vec`
[0]: https://rust.godbolt.org/z/GjcMYnEzq> But clang static analyzer will happily detect it.
One problem is that you're not guaranteed to catch it, similarly to why static analyzers aren't guaranteed to catch use-after-frees.
So funny thing, your C++ 26 solution doesn't do what my Rust function does.
Actually even the first one doesn't, but you likely don't really care about a std::unique_ptr, so it doesn't feel like a difference, and the negligence only really bites when it's not "just" that pointer type.
You see that Rust function destroyed the T. It's gone, no more T. But your C++ function makes a new T, then destroys the old one, so there's still a T, it's not gone after all.
Perhaps your idea of C++ semantics is a bit off?
Is that the joke here? That despite everything you didn't understand why core::mem::drop has that definition and so reading the empty body you assumed that you can just not do anything and that'll work in C++ ?
https://godbolt.org/z/zM3oxjrfn
and contrast this Rust:
https://rust.godbolt.org/z/1rMYcqY65
In your unique_ptr<T> example what you'd hidden (from me? Or perhaps from yourself) was that we're not destroying the unique_ptr, we're just destroying the Foo, and since the unique_ptr is null the destructor for that will be silent when the scope ends.
Entire API’s are designed around this: my type that tracks a background task can have a shutdown() method that moves self, so that if you call foo.shutdown(), you can’t use foo any more.
This is more than just preventing a value from being used, it also facilitates making a rule that “moves are only just memcpy()”, and it can actually be enforced:
C++ move semantics require you to write arbitrary code that pillages the moved-from value (like setting the heap pointer of a moved-from value to nullptr) to ensure the old value is safe: rust just says “nope, there’s no constructor, moves are just a memcpy. We will keep it safe by simply not letting code use the old value.”
C++ can never have this unless they offered yet another mutually incompatible form of move semantics (lol, what would the sigil be? &*&?)
I agree.
Probably depends on what you mean by "works out". I don't think GP would agree that delivering a less capable alternative qualifies.
For example, one major feature C++0x concepts was supposed to have but got removed was definition-time checking - i.e., checking that your template only used capabilities promised by the concepts it uses, so if you defined a template with concepts you could be assured that if the definition compiled it'd work with all types that satisfied the concept. That feature did not make it to C++20 concepts and as far as I know there are no plans on the horizon to add that feature.
C++26 concepts has more or less everything you mention, and you can try it out with all the major compilers right now.
C++ 0x is what people called the proposed new C++ language standard from about 2005 through 2009 or so under the belief that maybe it would ship in 2008 or 2009. Because you're here, now, you know this didn't end up happening and actually the next standard would be C++ 11. For a little while they even jokingly talked about C++ 0A where A is of course hexadecimal for ten, but by the time it was clear it wouldn't even make 2010 that wasn't funny.
So C++ 0x isn't five years ago, it's about 15-20 years ago and in this context it's about the draft revision of C++ in which for some time the Concepts feature existed, but Bjarne insisted that this feature (which remember is roughly Rust's traits) was not implementable in reasonable time, and frankly was not as much needed as people had believed.
This argument swayed enough committee members that Concepts was ripped back out of the draft document, and so C++ 11 does not have Concepts of any sort. Because this particular history is from the relatively recent past you can go read the proposal documents, there might even be Youtube videos about it.
OK, so, now you at least know what these terms mean when other people use them, that can't hurt.
As to your next claim er, no, not even close. Barry Revzin wrote a really nice paper connecting the dots on this, which probably passed into legend specifically for saying hey C++ 0x Concepts are the same thing as Rust traits. C++ proposal paper P2279 is what you're looking for if that interests you. That'll be less confusing for you now because you know what "C++ 0x" even means.
Now, Barry wrote that paper in the C++ 23 cycle, and we're now at / just past the end of the C++ 26 cycle, but I assure you that nothing relevant has changed. You can't magically have model checking in C++ that's not there. You can't provide concept maps, it's not in the language and so on.
You're quite a bit off. Tialaramex covered this well enough.
> C++26 concepts has more or less everything you mention, and you can try it out with all the major compilers right now.
Uh, no. No, it doesn't. Here's an example I wrote up earlier that demonstrates how concepts (still) don't have definition-time checking:
#include <concepts>
template<typename T>
concept fooable = requires(T t) {
{ t.foo() } -> std::same_as<int>;
};
struct only_foo {
int foo();
};
struct foo_and_bar {
int foo();
int bar();
};
template<fooable T>
int do_foo_bar(T t) {
t.bar(); // No definition-time error despite fooable not specifying the presence of bar()
return t.foo();
}
// Succeeds despite fooable only requiring foo()
template int do_foo_bar<foo_and_bar>(foo_and_bar t);
// Fails even though only_foo satisfies fooable
// template int do_foo_bar<only_foo>(only_foo t);
Here's Clang 21.1.0 compiling this in C++26 mode: https://cpp.godbolt.org/z/znPGvcTqs . Note that as-is the snippet compiles fine, but if you uncomment the last line you get an error despite only_foo satisfying fooable.Contrast this with Rust:
trait Fooable { fn foo(self) -> i32; }
fn do_foo_bar<T: Fooable>(t: T) -> i32 { let _ = t.bar(); // error[E0599]: no method named `bar` found for type parameter `T` in the current scope t.foo() }
Notice how do_foo_bar didn't need to be instantiated for the compiler to catch the error. That's what C++ concepts are unable to do, and as far as I know there is nothing on the horizon to change that.
But as it stands, I expect that whatever innovations these languages produce will be picked up by C++ in ~5 years.
This is exactly WHY we dont see a rush movement of C++ developers to Rust throwing away everything for Rust. Rust is trying to solve problems that already not exist 99.9999% of time in modern C++ code style and standards.
Also, some day C++ compilers or tooling will get its own Borrow Checker to completely forget about Rust - this will be done just for fun just to stop arguing with rust-fans :)
No amount of fallible human vigilance will stop you from forgetting the existence of a C++ quirk in the code you're rushing out before heading out for the night. Human oversight does not scale.
Rust solves 1 category of problems in a way that is not without its costs and other consequences. That is it. There are projects where this is very important, there are other projects where its virtually useless and the consequences just get in the way. It is not magic. It doesn't make anything actually 'safe'.
That being said, Rust is really about lifetimes. That's the big ticket selling point. My point above was that 1) it isn't a silver bullet and 2) it can be a real hindrance is many applications.
I bet there's easily tens of thousands of times more C++ code than Rust code out there.
The number of people I met in Rust conferences that rewriting at least parts of rather big C++ codebases weren't small either.
However, there is still big amount of code that is purely C++. Many of the older code bases still use C++03-style code too. Or they were written in the OOP design pattern golden era that requires huge reactors to adapt functional / modern code. Anything with Qt will not benefit from smart pointers. Even with Qt 6.
Rust cannot solve these problems since the challenges are not purely technical but social too.