Matt Godbolt sold me on Rust by showing me C++
135 comments
·May 6, 2025dvratil
jeroenhd
The result type does make for some great API design, but SerenityOS shows that this same paradigm also works fine in C++. That includes something similar to the ? operator, though it's closer to a raw function call.
SerenityOS is the first functional OS (as in "boots on actual hardware and has a GUI") I've seen that dares question the 1970s int main() using modern C++ constructs instead, and the API is simply a lot better.
I can imagine someone writing a better standard library for C++ that works a whole lot like Rust's standard library does. Begone with the archaic integer types, make use of the power your language offers!
If we're comparing C++ and Rust, I think the ease of use of enum classes/structs is probably a bigger difference. You can get pretty close, but Rust avoids a lot of boilerplate that makes them quite usable, especially when combined with the match keyword.
I think c++, the language, is ready for the modern world. However, c++, the community, seems to be struck at least 20 years in the past.
jchw
Google has been doing a very similar, but definitely somewhat uglier, thing with StatusOr<...> and Status (as seen in absl and protobuf) for quite some time.
A long time ago, there was talk about a similar concept for C++ based on exception objects in a more "standard" way that could feasibly be added to the standard library, the expected<T> class. And... in C++23, std::expected does exist[1], and you don't need to use exception objects or anything awkward like that, it can work with arbitrary error types just like Result. Unfortunately, it's so horrifically late to the party that I'm not sure if C++23 will make it to critical adoption quickly enough for any major C++ library to actually adopt it, unless C++ has another massive resurgence like it did after C++11. That said, if you're writing C++ code and you want a "standard" mechanism like the Result type, it's probably the closest thing there will ever be.
a_t48
There’s a few backports around, not quite the same as having first class support, though.
dvt
Maybe contrarian, but imo the `Result` type, while kind of nice, still suffers from plenty of annoyances, including sometimes not working with the (manpages-approved) `dyn Error`, sometimes having to `into()` weird library errors that don't propagate properly, or worse: `map_err()` them; I mean, at this point, the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start. Also, `?` doesn't work in closures, etc.
So, while this is an improvement over C++ (and that is not saying much at all), it's still implemented in a pretty clumsy way.
singingboyo
There's some space for improvement, but really... not a lot? Result is a pretty basic type, sure, but needing to choose a dependency to get a nicer abstraction is not generally considered a problem for Rust. The stdlib is not really batteries included.
Doing error handling properly is hard, but it's a lot harder when error types lose information (integer/bool returns) or you can't really tell what errors you might get (exceptions, except for checked exceptions which have their own issues).
Sometimes error handling comes down to "tell the user", where all that info is not ideal. It's too verbose, and that's when you need anyhow.
In other cases where you need details, anyhow is terrible. Instead you want something like thiserror, or just roll your own error type. Then you keep a lot more information, which might allow for better handling. (HttpError or IoError - try a different server? ParseError - maybe a different parse format? etc.)
So I'm not sure it's that Result is clumsy, so much that there are a lot of ways to handle errors. So you have to pick a library to match your use case. That seems acceptable to me?
FWIW, errors not propagating via `?` is entirely a problem on the error type being propagated to. And `?` in closures does work, occasionally with some type annotating required.
maplant
? definitely works in closures, but it often takes a little finagling to get working, like specifying the return type of the closure or setting the return type of a collect to a Result<Vec<_>>
null
ackfoobar
> the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start
If you use `anyhow`, then all you know is that the function may `Err`, but you do not know how - this is no better than calling a function that may `throw` any kind of `Throwable`. Not saying it's bad, it is just not that much different from the error handling in Kotlin or C#.
jbritton
I know a ‘C’ code base that treats all socket errors the same and just retries for a limited time. However there are errors that make no sense to retry, like invalid socket or socket not connected. It is necessary to know what socket error occurred. I like how the Posix API defines an errno and documents the values. Of course this depends on accurate documentation.
efnx
Yes. I prefer ‘snafu’ but there are a few, and you could always roll your own.
zozbot234
> The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions
This isn't really true since Rust has panics. It would be nice to have out-of-the-box support for a "no panics" subset of Rust, which would also make it easier to properly support linear (no auto-drop) types.
kelnos
I wish more people (and crate authors) would treat panic!() as it really should be treated: only for absolutely unrecoverable errors that indicate that some sort of state is corrupted and that continuing wouldn't be safe from a data- or program-integrity perspective.
Even then, though, I do see a need to catch panics in some situations: if I'm writing some sort of API or web service, and there's some inconsistency in a particular request (even if it's because of a bug I've written), I probably really would prefer only that request to abort, not for the entire process to be torn down, terminating any other in-flight requests that might be just fine.
But otherwise, you really should just not be catching panics at all.
tcfhgj
would you consider panics acceptable when you think it cannot panic in practice? e.g. unwraping/expecting a value for a key in a map when you inserted that value before and know it hasn't been removed?
you could have a panic though, if you wrongly make assumptions
codedokode
It's pretty difficult to have no panics, because many functions allocate memory and what are they supposed to do when there is no memory left? Also many functions use addition and what is one supposed to do in case of overflow?
PhilipRoman
>what are they supposed to do when there is no memory left
Well on Linux they are apparently supposed to return memory anyway and at some point in the future possibly SEGV your process when you happen to dereference some unrelated pointer.
Arnavion
>many functions allocate memory and what are they supposed to do when there is no memory left?
Return an AllocationError. Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. It's now trying to add in explicit allocators and allocation failure handling (A:Alloc type param) at the cost of splitting the ecosystem (all third-party code, including parts of libstd itself like std::io::Read::read_to_end, only work with A=GlobalAlloc).
Zig for example does it right by having explicit allocators from the start, plus good support for having the allocator outside the type (ArrayList vs ArrayListUnmanaged) so that multiple values within a composite type can all use the same allocator.
>Also many functions use addition and what is one supposed to do in case of overflow?
Return an error ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) or a signal that overflow occurred ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ). Or use wrapping addition ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) if that was intended.
Note that for the checked case, it is possible to have a newtype wrapper that impls std::ops::Add etc, so that you can continue using the compact `+` etc instead of the cumbersome `.checked_add(...)` etc. For the wrapping case libstd already has such a newtype: std::num::Wrapping.
Also, there is a clippy lint for disallowing `+` etc ( https://rust-lang.github.io/rust-clippy/master/index.html#ar... ), though I assume only the most masochistic people enable it. I actually tried to enable it once for some parsing code where I wanted to enforced checked arithmetic, but it pointlessly triggered on my Checked wrapper (as described in the previous paragraph) so I ended up disabling it.
nicce
Additions are easy. By default they are wrapped, and you can make them explicit with checked_ methods.
Assuming that you are not using much recursion, you can eliminate most of the heap related memory panics by adding limited reservation checks for dynamic data, which is allocated based on user input/external data. You should also use statically sized types whennever possible. They are also faster.
pdimitar
Don't know about your parent poster but I didn't take it 100% literally. Obviously if there's no memory left then you crash; the kernel would likely murder your program half a second later anyway.
But for arithmetics Rust has non-aborting bound checking API, if my memory serves.
And that's what I'm trying hard to do in my Rust code f.ex. don't frivolously use `unwrap` or `expect`, ever. And just generally try hard to never use an API that can crash. You can write a few error branches that might never get triggered. It's not the end of the world.
arijun
`panic` isn’t really an error that you have to (or can) handle, it’s for unrecoverable errors. Sort of like C++ assertions.
Also there is the no_panic crate, which uses macros to require the compiler to prove that a given function cannot panic.
marcosdumay
Well, kinda. It's more similar to RuntimeException in Java, in that there are times where you do actually want to catch and recover from them.
But on those places, you better know exactly what you are doing.
nicce
I would say that Segmentation Fault is better comparison with C++ :-D
alexeldeib
that's kind of a thing with https://docs.rs/no-panic/latest/no_panic/ or no std and custom panic handlers.
not sure what the latest is in the space, if I recall there are some subtleties
zozbot234
That's a neat hack, but it would be a lot nicer to have explicit support as part of the language.
0x1ceb00da
Proper error handling is the biggest problem in a vast majority of programs and rust makes that straightforward by providing a framework that works really well. I hate the `?` shortcut though. It's used horribly in many rust programs that I've seen because the programmers just use it as a half assed replacement for exceptions. Another gripe I have is that most library authors don't document what errors are returned in what situations and you're left making guesses or navigating through the library code to figure this out.
kccqzy
The Result type isn't really enough for fun and easy error handling. I usually also need to reach for libraries like anyhow https://docs.rs/anyhow/latest/anyhow/. Otherwise, you still need to think about the different error types returned by different libraries.
Back at Google, it was truly an error handling nirvana because they had StatusOr which makes sure that the error type is just Status, a standardized company-wide type that stills allows significant custom errors that map to standardized error categories.
mdf
Generally, I agree the situation with errors is much better in Rust in the ways you describe. But, there are also panics which you can catch_unwind[1], set_hook[2] for, define a #[panic_handler][3] for, etc.
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
ekidd
Yeah, in anything but heavily multi-threaded servers, it's usually best to immediately crash on a panic. Panics don't mean "a normal error occurred", they mean, "This program is cursed and our fundamental assumptions are wrong." So it's normal for a unit test harness to catch panics. And you may occasionally catch them and kill an entire client connection, sort of the way Erlang handles major failures. But most programs should just exit immediately.
jasonjmcghee
unfortunately it's not so simple. that's the convention. depending on the library you're using it might be a special type of Error, or special type of Result, something needs to be transformed, `?` might not work in that case (unless you transform/map it), etc.
I like rust, but its not as clean in practice, as you describe
ryandv
There are patterns to address it such as creating your own Result type alias with the error type parameter (E) fixed to an error type you own:
type Result<T> = result::Result<T, MyError>;
#[derive(Debug)]
enum MyError {
IOError(String)
// ...
}
Your owned (i.e. not third-party) Error type is a sum type of error types that might be thrown by other libraries, with a newtype wrapper (`IOError`) on top.Then implement the `From` trait to map errors from third-party libraries to your own custom Error space:
impl From<io::Error> for MyError {
fn from(e: io::Error) -> MyError {
MyError::IOError(e.to_string())
}
}
Now you can convert any result into a single type that you control by transforming the errors: return sender
.write_all(msg.as_bytes())
.map_err(|e| e.into());
There is a little boilerplate and mapping between error spaces that is required but I don't find it that onerous.null
null
koakuma-chan
You can use anyhow::Result, and the ? will work for any Error.
fpoling
Result type still requires quite a few lines of boilerplate if one needs to add custom data to it. And as a replacement of exceptions with automatic stack trace attachment it is relatively poor.
In any case I will take Rust Result over C++ mess at any time especially given that we have two C++, one with exception support and one without making code incompatible between two.
thrwyexecbrain
The C++ code I write these days is actually pretty similar to Rust: everything is explicit, lots of strong types, very simple and clear lifetimes (arenas, pools), non-owning handles instead of pointers. The only difference in practice is that the build systems are different and that the Rust compiler is more helpful (both in catching bugs and reporting errors). Neither a huge deal if you have a proper build and testing setup and when everybody on your team is pretty experienced.
By the way, using "atoi" in a code snippet in 2025 and complaining that it is "not ideal" is, well, not ideal.
kasajian
This seems a big silly. This is not a language issue. You can have a C++ library that does exactly all the things being shown here so that the application developer doesn't worry about. There would no C++ language features missing that would accomplish what you're able to do on the Rust side.
So is this really a language comparison, or what libraries are available for each language platform? If the latter, that's fine. But let's be clear about what the issue is. It's not the language, it's what libraries are included out of the box.
lytedev
The core of this argument taken to its extreme kind of makes the whole discussion pointless, right? All the languages can do all the things, so why bother differentiating them?
To entertain the argument, though, it may not be a language issue, but it certainly is a selling point for the language (which to me indicates a "language issue") to me if the language takes care of this "library" (or good defaults as I might call them) for you with no additional effort -- including tight compiler and tooling integration. That's not to say Rust always has good defaults, but I think the author's point is that if you compare them apples-to-oranges, it does highlight the different focuses and feature sets.
I'm not a C++ expert by any stretch, so it's certainly a possibility that such a library exists that makes Rust's type system obsolete in this discussion around correctness, but I'm not aware of it. And I would be incredibly surprised if it held its ground in comparison to Rust in every respect!
sdenton4
If the default is a loaded gun pointed at your foot, you're going to end up with lots of people missing a foot. "just git gud" isn't a solution.
cbsmith
That's an entirely different line of reasoning from the article though, and "just git gud" isn't really the solution here any more than it is to use Rust. There are facilities for avoiding these problems that you don't have to learn how to construct yourself in either language.
Etheryte
Just like language shapes the way we think and talk about things, programming languages shape both what libraries are written and how. You could write anything in anything so long as it's Turing complete, but in real life we see clearly that certain design decisions at the language level either advantage or disadvantage certain types of solutions. Everyone could in theory write C without any memory issues, but we all know how that turns out in practice. The language matters.
cbsmith
Yeah, I kept thinking, "doesn't mp-units basically address this entirely"?
jpc0
Amazing example of how easy it is to get sucked into the rust love. Really sincerely these are really annoying parts of C++.
The conversation function is more language issue. I don’t think there is a simple way of creating a rust equivalent version because C++ has implicit conversions. You could probably create a C++ style turbofish though, parse<uint32_t>([your string]) and have it throw or return std::expected. But you would need to implement that yourself, unless there is some stdlib version I don’t know of.
Don’t conflate language features with library features.
And -Wconversion might be useful for this but I haven’t personally tried it since what Matt is describing with explicit types is the accepted best practice.
grumbel
There is '-Wconversion' to catch things like this. It will however not trigger in this specific case since g++ assumes converting 1000.0 to 1000 is ok due to no loss in precision.
Quantity(100) is counterproductive here, as that doesn't narrow the type, it does the opposite, it casts whatever value is given to the type, so even Quantity(100.5) will still work, while just plain 100.5 would have given an error with '-Wconversion'.
Arnavion
The reason to introduce the Quantity wrapper is to not be able to swap the quantity and price arguments.
b5n
> -Wconversion ... assumes converting 1000.0 to 1000 is ok due to no loss in precision.
Additionally, `clang-tidy` catches this via `bugprone-narrowing-conversions` and your linter will alert if properly configured.
kelnos
My opinion is that if you need to run extra tools/linters in order to catch basic errors, the language & its compiler are not doing enough to protect me from correctness bugs.
I do run clippy on my Rust projects, but that's a matter of style and readability, not correctness (for the most part!).
jpc0
How much of what Rust the language checks is actually linter checks implemented in the compiler?
Conversions may be fine and even useful in many cases, in this case it isn’t. Converting to std::variant or std::optional are some of those cases that are really nice.
GardenLetter27
It's a shame Rust doesn't have keyword arguments or named tuples to make handling some of these things easier without Args/Options structs boilerplate.
frankus
I work all day in Swift (which makes you go out of your way to omit argument labels) and I'm surprised they aren't more common.
jsat
Had the same thought... It's backwards that any language isn't using named parameters at this point.
shpongled
Yep, I would love anonymous record types, ala StandardML/OCaml
tumdum_
The one thing that sold me on Rust was that I no longer had to chase down heisenbugs caused by memory corruption.
writebetterc
Yes, Rust is better. Implicit numeric conversion is terrible. However, don't use atoi if you're writing C++ :-). The STL has conversion functions that will throw, so separate problem.
titzer
> Implicit numeric conversion is terrible.
It's bad if it alters values (e.g. rounding). Promotion from one number representation to another (as long as it preserves values) isn't bad. This is trickier than it might seem, but Virgil has a good take on this (https://github.com/titzer/virgil/blob/master/doc/tutorial/Nu...). Essentially, it only implicitly promotes values in ways that don't lose numeric information and thus are always reversible.
In the example, Virgil won't let you pass "1000.00" to an integer argument, but will let you pass "100" to the double argument.
plus
Aside from the obvious bit size changes (e.g. i8 -> i16 -> i32 -> i64, or f32 -> f64), there is no "hierarchy" of types. Not all ints are representable as floats. u64 can represent up to 2^64 - 1, but f64 can only represent up to 2^53 with integer-level precision. This issue may be subtle, but Rust is all about preventing subtle footguns, so it does not let you automatically "promote" integers to float - you must be explicit (though usually all you need is an `as f64` to convert).
mananaysiempre
> Aside from the obvious bit size changes (e.g. i8 -> i16 -> i32 -> i64, or f32 -> f64), there is no "hierarchy" of types.
Depends on what you want from such a hierarchy, of course, but there is for example an injection i32 -> f64 (and if you consider the i32 operations to be undefined on overflow, then it’s also a homomorphism wrt addition and multiplication). For a more general view, various Schemes’ takes on the “numeric tower” are informative.
favorited
Side note, if anyone is interested in hearing more from Matt, he has a programming podcast with Ben Rady called Two's Complement.
badbart14
+1, especially loved the episode from a couple months back about using AI tools in development. Really got me thinking differently about the role of AI in a developer's workflow and how software development will evolve.
penguin_booze
I can't see the publish date on the episodes.
gbin
Interestingly in Rust I would immediately use an Enum for the Order! Way more powerful semantically.
markus_zhang
What if we have a C that removes the quirks without adding too much brain drain?
So no implicit type conversions, safer strings, etc.
LorenDB
Walter Bright will probably show up soon to plug D's BetterC mode, but if he doesn't, still check it out.
cogman10
I've seen this concept tried a few times (For example, MS tried it with Managed C++). The inevitable problem you run into is any such language isn't C++. Because of that, you end up needing to ask, "why pick this unpopular half C/C++ implementation and not Rust/go/D/Java/python/common lisp/haskell."
A big hard to solve problem is you are likely using a C because of the ecosystem and/or the performance characteristics. Because of the C header/macro situation that becomes just a huge headache. All the sudden you can't bring in, say, boost because the header uses the quirks excluded from your smaller C language.
nitwit005
Because it's easier to add a warning or error. Don't like implicit conversions? Add a compiler flag, and the issue is basically gone.
Safer strings is harder, as it gets into the general memory safety problem, but people have tried adding safer variants of all the classic functions, and warnings around them.
mamcx
If you can live without much of the ecosystem (specially if has async) there is way to write rust very simple.
The core of Rust is actually very simple: Struct, Enum, Functions, Traits.
o11c
I too have been thinking a lot about a minimum viable improvement over C. This requires actually being able to incrementally port your code across:
* "No implicit type conversions" is trivial, and hardly worth mentioning. Trapping on both signed and unsigned overflow is viable but for hash-like code opting in to wrapping is important.
* "Safer strings" means completely different things to different people. Unfortunately, the need to support porting to the new language means there is little we can do by default, given the huge amount of existing code. We can however, add new string types that act relatively uniformly so that the code can be ported incrementally.
* For the particular case of arrays, remember that there are at least 3 different ways to compute its length (sentinel, size, end-pointer). All of these will need proper typing support. Particularly remember functions that take things like `(begin, middle end)`, or `(len, arr1[len], arr2[len])`.
* Support for nontrivial trailing array-or-other datums, and also other kinds of "multiple objects packed within a single allocation", is essential. Again, most attempted replacements fail badly.
* Unions, unfortunately, will require much fixing. Most only need a tag logic (or else replacement with bitcasting), but `sigval` and others like it are fundamentally global in nature.
* `va_list` is also essential to support since it is very widely used.
* The lack of proper C99 floating-point support, even in $CURRENTYEAR, means that compile-to-C implementations will not be able to support it properly either, even if the relevant operations are all properly defined in the new frontend to take an extra "rounding mode" argument. Note that the platform ABI matters here.
* There are quite a few things that macros are used for, but ultimately this probably is a finite set so should be possible to automatically convert with a SMOC.
Failure to provide a good porting story is the #1 mistake most new languages make.
wffurr
This seems like such an obvious thing to have - where is it? Zig, Odin, etc. all seem much more ambitious.
steveklabnik
There have been attempts over the years. See here, a decade ago: https://blog.regehr.org/archives/1287
> eventually I came to the depressing conclusion that there’s no way to get a group of C experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on the Friendly C dialect. There are just too many variations, each with its own set of performance tradeoffs, for consensus to be possible.
IshKebab
I think if you are going to fix C's footguns you'll have to change so much you end up with a totally new language anyway, and then why not be ambitious? It costs a lot to learn a new language and people aren't going to bother if the only benefit it brings is things that can sort of mostly be caught with compiler warnings and static analysis.
zyedidia
I think the only "C replacement" that is comparable in complexity to C is [Hare](https://harelang.org/), but several shortcomings make it unsuitable as an actual C replacement in many cases (little/no multithreading, no support for macOS/Windows, no LLVM or GCC support, etc.).
alexchamberlain
I'm inferring that you think Rust adds too much brain drain? If so, what?
GardenLetter27
The borrow checker rejects loads of sound programs - just read https://rust-unofficial.github.io/too-many-lists/
Aliasing rules can also be problematic in some circumstances (but also beneficial for compiler optimisations).
And the orphan rule is also quite restrictive for adapting imported types, if you're coming from an interpreted language.
https://loglog.games/blog/leaving-rust-gamedev/ sums up the main issues nicely tbh.
IshKebab
> The borrow checker rejects loads of sound programs
I bet assembly programmers said the same about C!
Every language has relatively minor issues like these. Seriously pick a language and I can make a similar list. For C it will be a very long list!
leonheld
I love Rust, but I after doing it for a little while, I completely understand the "brain drain" aspect... yes, I get significantly better programs, but it is tiring to fight the borrow-checker sometimes. Heck, I currently am procrastinating instead of going into the ring.
Anyhow, I won't go back to C++ land. Better this than whatever arcane, 1000-line, template-hell error message that kept me fed when I was there.
dlachausse
Swift is really great these days and supports Windows and Linux. It almost feels like a scripting language other than the compile time of course.
smt88
There is no universe where I'm doing to use Apple tooling on a day to day basis. Their DX is the worst among big tech companies by far.
dlachausse
They have quite robust command line tooling and a good VS Code plugin now. You don’t need to use Xcode anymore for Swift.
jsat
I see an article about how strict typing is better, but what would really be nice here is named parameters. I never want to go back to anonymous parameters.
kelnos
Yes, this is one of the few things that I think was a big mistake in Rust's language design. I used to do a lot of Scala, and really liked named parameters there.
I suppose it could still be added in the future; there are probably several syntax options that would be fully backward-compatible, without even needing a new Rust edition.
codedokode
When there are 3-4 parameters it is too much trouble to write the names.
The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions, functions returning bool, functions returning 0 on success, functions returning 0 on error, functions returning -1 on error, functions returning negative errno on error, functions taking optional pointer to bool to indicate error (optionally), functions taking reference to std::error_code to set an error (and having an overload with the same name that throws an exception on error if you forget to pass the std::error_code)...I understand there's 30 years of history, but it still is annoying, that even the standard library is not consistent (or striving for consistency).
Then you top it on with `?` shortcut and the functional interface of Result and suddenly error handling becomes fun and easy to deal with, rather than just "return false" with a "TODO: figure out error handling".