Rust Weird Expressions
github.com
github.com
> What's your favourite @rustlang boolean? Option<()>, Option<Option<!>>, std::iter::Once<()>, std::fmt::Result
> tired: u16, wired: BTreeSet<BTreeSet<BTreeSet<BTreeSet<()>>>>
> What's your favourite @rustlang integer? &[()], Enumerate<Repeat<()>>, Poll<File>, *const PhantomData<usize>
@jaaaarwr> How is Vec<()> not in this poll?
> That's just the owning version of &[()], right? Why would you need to own ()s? ;)
Edit: She's also the author of inline_python!, a serious macro for interspersing code that calls python (including local variable interop), and whichever_compiles!, a joke macro that takes multiple expressions, forks the compiler (as in a fork syscall), and compiles the first one that compiles to valid rust.
A lot of people are on board with not using if/else and instead :)
match x {
true => {}
false => {}
}EDIT: quoted wrong part of parent, ignore above
> match x { true => {} false => {} }
I really like this. Pattern matching syntax is great, nice to use in place of if/else as much as possible
There's a nice alternative though, which is empty struct errors.
#[derive(Debug, thiserror::Error, displays of::Display)]
/// Frobnicating the foo failed.
struct FrobError;
The advantage is the standard advantages types use, i.e. if I want to propagate it I know this specific zero sized type has a specific meaning. It'll also show a nice message if you print it. let plural = match cases.len() > 1 {
true => "s",
false => "",
};
Of course, you'd more likely have something like let prefix = match cases.len() {
0 => "no values".to_string(),
1 => "a value".to_string(),
n => format!("{} values", n),
}; let plural = match cases.len() {
1.. => "s",
0 => "",
};
;P let plural = match cases.len() {
0 | 2.. => "s",
1 => "",
};
or something (though to be fair the original code snippet also handled "0" incorrectly -- should be "cases.len() != 1").This is inspired.
> Did you know you can just fork() the @rustlang compiler from your proc_macro? :D
> This is, of course, a horrible idea.
> Unfortunately rustc is multithreaded and fork() only forks one thread. We'll just assume the other threads weren't doing anything important.
> In today's rustc, this means we're only missing the main thread in the fork. But that one was only going to call exit() after joining our thread anyway, so not very important. It just means the process always exits with a success status. So instead we grep the output for "error".
Any my favorite reply:
> Is this a rust implementation of autoconf?
https://mobile.twitter.com/m_ou_se/status/136863270144881869...
fn eject() -> i32 {
return return return return return return!!!!!!!!11;
}To clarify: the expression will run, but it has the type `!` ("never"). And while that's still not a "proper" rust type (other uninhabited types are though they may not be quite as special-cased in the compiler), it is in essence a bottom type: since it can't have a value, it will unify with any type.
That's why Rust is perfectly happy with
let foo = if a { 1 } else { return 42 };
The first branch has type `{integer}`, the second branch has type `!`. `!` unifies just fine with `{integer}`, therefore the typechecker is happy.Rust has a syntax called "turbofish" for disambiguating ambiguous generic calls. (Coined by Anna Harren on twitter, the first reply is an illustration from Karen Rustad Tölva, the inventor of Ferris [1]).
Although Vec is generic over any item type, you can normally just write
let foo = Vec::new();
If you're doing something complicated where the compiler can't infer the type, you instead use the turbofish syntax let foo = Vec::<u32>::new();
Some people hate this syntax. One of the better arguments I've seen against it comes (I think) from ManishEarth, who points out that it's very hard for the compiler to detect that incorrect attempts at writing a turbofish are in fact attempts at the turbofish and offer help.That test case (minus some excellent poetry) is
let (oh, woe, is, me) = ("the", "Turbofish", "remains", "undefeated");
let _: (bool, bool) = (oh<woe, is>(me));
That is actually comparing the string "the" against the string "Turbofish" and the string "remains" against the string "undefeated". The test case is there to point out that if the "::" in the turbofish is removed (Vec<u32>::new() instead of Vec::<u32>::new()) there will be cases where it's ambiguous.[1] https://twitter.com/whoisaldeka/status/914914008225816576?la...
EDIT: Fix my misspelling of ManishEarth
https://github.com/rust-lang/rfcs/pull/2527#issuecomment-414...
It wasn't until that NYT (or whichever paper) interview with 'Dan G[...] HN moderator' that it clicked.
(I still frequently hear it wrong in my head. Habits..)
You raise a good point. I've had a few emails addressing me 'OJ' Ford, which is beyond reasonable in hindsight, just not something I foresaw or that anyone's ever called me in person! Perhaps I unwittingly contributed to that by registering my username mildly against the cultural grain.. :)
Yours,
O. Johnson-Fsmith
Operational Research Department
Over the years, many have mistook my account for that of Sam Altman (sama), or presumed it to be a parody thereof.
In my defense, I have been using this handle, and variants of it, for longer than he's been alive.
Best, (Maha) Sam Atman "you can choose your friends but not your namesake"
I do wonder, if the whitespace (or lack thereof) was mandatory, could this be resolved? What would be the impact of comparisons needing whitespace?
-- "The Humble Programmer"
https://www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340...
Practically, it's exactly what comes out of the infamous Rust vs. Go debate: for instance, the former prefers a short and concise albeit arguably more complex encoding and processing of the errors based on a monadic type, whereas the other leans toward a simpler, but more verbose, double return value + if/else/early return. Chose your poison.
A more elegant language that solves the problems that Rust solves, with a production-ready implementation and a good ecosystem hasn't been presented to the world yet. It's to be expected that if you want to benefit from its advantages, you need to pay the cost of dealing with a more complex language. On the other hand paying the cost of being exposed to more concepts pays off by giving you more flexibility to craft simple solutions. Complexity of a project can vastly exceed that of the language the project is written it. In that case the cognitive cost of using more advanced tools to manage that complexity is a boon in the long run.
This is analogous to any kind of more sophisticated technology, e.g. you can argue that a scythe is easier to use than a harvester, but with a big enough field, if you have the option, you'll go for the latter.
He was right; there was a great future for that (and everything else in computing). But perhaps he didn't anticipate the FAR greater corporate appetite for functionality. The next feature is paramount. Who will code this next feature? The only path was richer and more powerful languages.
But this game's configuration requires us to play, compels us to play. And so we play to lose, to continue the game.
fn main() { break rust; }
This breaks rust =) fn punch_card() -> impl std::fmt::Debug {
This is a function that returns some type that implements Debug. Not specifically named.The rest of it is a combination of two syntaxes: .. and ..=
These are both ranges. The former exclusive, the latter inclusive. You can write something like 1..5 (or 1..=5) to get a range from 1 to 5 inclusive/exclusive, but you can also leave the start or end off: 2.. will give you from 1 till the end, ..=5 will give you from the start to 4. This means that .. will give you the full range. It looks more normal when used in context:
fn main() {
let s = "some string";
let some = &s[..=4]; // you'd probably write ..5 but i wanted to show both syntaxes
let string = &s[5..];
let all = &s[..]; // a no-op here but useful if s were a String and you wanted 'all' to be a &str
println!("{}", some);
println!("{}", string);
}
prints "some" and then "string".So, here's the tricky bit: you can overload what the range is over. They're not just for numbers. So:
fn main() {
let mut string = 'a'..='z';
let a = string.next();
let b = string.next();
println!("{:?}", a);
println!("{:?}", b);
}
prints "Some('a')" and then "Some('b')", because iterators return Options.So... all of this means that, you can implement a range of ranges. And so this string builds up a ridiculous unbounded range of unbounded range of unbounded range of ...
Personally, I agree that Rust is complex, but almost all of that complexity is inherent to the kinds of tradeoffs Rust makes. With different tradeoffs, I can certainly imagine a much simpler language, but it's less clear to me what stuff could be significantly simplified without doing so. However, I am extremely biased!
1. 90% of the time, rust module layouts are basically a copy of the file-system heirarchy, so why do I have to type this? This system would be much more approachable if it gave the file-system hierarchy as the default module layout, and let you override it explicitly. The argument I have heard against this is that some people like to comment out module declarations during debugging. I don't find this compelling, because I don't see why you couldn't have an explicit way to ignore a module instead. This is just one of many cases where Rust seems to prioritize the edge case at the expense of the common case.
2. On top of requiring a lot of boilerplate the module system is quite esoteric. I've had to learn it twice: a few years ago I was dabbling in Rust, and I remember having to struggle a bit to understand how to add a second file to my project, and then about a year ago when I was getting back into rust I found it unintuitive a second time. I challenge you to find someone who is unfamiliar with the module system, and see how long it takes them, using only the documentation, to figure out where they have to put the `mod` declarations to add nested submodules to their project to make it compile. Maybe I am unreasonably thick, but I don't think it's my problem since I've worked with a lot of languages, and never had this much trouble.
3. This esoteric system isn't even deterministic. Imagine I have the line `mod foo` in my `lib.rs`. Where can I go to find the source for that? Well, it depends. It could be in `src/foo.rs`, or it could be in `src/foo/mod.rs`. And let's say I'm using external crate: `use some_crate`. Which import does that correspond to? It could be: `some_crate`, or it could be `some-crate` in my `Cargo.toml`. You just have to kind of know all of these implicit behaviors of the compiler to know what's going on.
So this is just one feature, but IMO it's just one example of a case where Rust puts very little emphasis on the UX and understandability of the language.
There are some good reasons and interesting arguments around all this, but given that I was just curious about your opinion, I won't bore you with all that :) Thanks!
I think it's debatable whether the module system really is complex or just different from what newcomers are used to (and by now I've grown pretty accustomed to it). But in contrast to most other language features, where it was clear what I was getting in return for the steep learning curve, the module system seemed overly complicated at the time for no real benefit. Not a big issue by any means and I would choose Rust with its module system over the alternatives most days of the week, but it is one tradeoff that to me at least seemed orthogonal to the other borrowing-related complexities.
(What is more worrying to me nowadays is the whole async story. I do hope that some of it will get better once certain features land and it is certainly an area where some additional complexity is unavoidable, but it is the only part of Rust that I dread touching despite heavily using async in a moderately sized personal project due to the need for WASM + IndexedDB, simply because lifetime issues become much more tricky once async and either traits, recursion or closures are involved. By now I am consciously trying to limit any async parts of the program to a simple and stupid "Rust-light" style without any "fancy" features such as traits or closures, which does not feel like a proper solution. So yeah, in general I agree with you: Rust is certainly complex, but for the most part not unnecessarily so.)
[1]: https://rust-lang.github.io/wg-async-foundations/vision.html
[2]: https://blog.rust-lang.org/2021/03/18/async-vision-doc.html
[3]: https://github.com/rust-lang/wg-async-foundations/pulls
[4]: https://github.com/rust-lang/wg-async-foundations/issues
Yeah, teaching the module system is kind of my white whale. Carol and I have spent more time on that part of the book than almost any other; re-written like five times.
My current working theory is that most people assume that "the module system" is similar to whatever one they've used in the past, then run into problems, and leads to frustration. I've talked to so many people who have totally opposite problems with it, with no real pattern to issues or expectations.
I think that it's very straightforward, personally, with very very simple rules (especially in 2018). But I certainly acknowledge that I am the exception, not the rule.
I agree with you. It's a shame we don't have a simple visual metaphor to describe this
EDIT: Wait does hacker news just delete non-ascii? I tried to put an upside down smiley here how is this even worse than I thought
When I treated modules as Java packages or C# namespaces I hated them. Only when I realized that I can treat them as glorified C imports did they start to make sense. I still hate them, but at least I can rationalize their design now.
I disagree that this is bad, and I think you've made a good decision.
I find that too many Rust programmers reach for a closure WAY too often and, even when they should grab a closure, they make it far too complicated. Closures should be short. Inline closures are nice when they're a single line part of "collect()" (although, I've seen some that make me want to hang the author ...).
However, if your closure is 15 lines long invoking a builder chain (this is a common issue in EventLoop type closures), that should be a function. This is before I get started about how builder chains are a gigantic misfeature to paper over the fact that the language doesn't have default/named function arguments.
Anyway ... I have found that "make it a named function and call it" is often a far better way to communicate exactly what your intent was.
Totally agree. I think it's telling that Rust Analyzer basically inserts argument labels inline in the editor, and this is the preferred way to work with Rust.
(In general, if you find yourself inventing falsehoods about other languages to promote Rust, you are Doing That Wrong, too.)
I have personally spent more time, summed over the past decade, preparing bug reports against Gcc than in debugging C++ memory-usage faults.
But C++ compiles faster.
Coding Rust is fun in much the way that Forth is: it is a charge to figure out a way to achieve a thing; you know it will ultimately be possible, and when you find the way, it is an ego boost. You could say, "Rust is the Second Programming Religion", and who could ever honestly disagree?
(But watch extremists downvote this to oblivion anyway.)
Genuine noob question: can you really compare Rust's borrow checker and lifetime management to the smart pointers in C++?
C++'s smart pointers have direct analogues in Rust, though there are some differences. (unique_ptr<T> and Box<T>, shared_ptr<T> and Rc<T>/Arc<T>)
The borrow checker is something else completely. The closest analogue in C++ is the https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines, which offer some similar kinds of checks to Rust, but don't attempt to go nearly as far as Rust does.
In modern C++ you get compile-time correctness by construction: you do things that reduce to correct code. All of the low-level mistakes remain possible, but are not tempting. Low-level, C-like operations are like writing "unsafe" in Rust; you can if you want to, or need to, but they are uglier, and anyway almost always not needed. Just as when coding a Rust "unsafe" block: if it ever is, you use extra care.
In a very real sense, C++ libraries perform in the role of the Rust compiler by insulating you from operations that are risky. The operations a good library exposes are safe. A Rust compiler bug could expose you to a memory fault, in the same way that a C++ library bug could; but the Rust compiler is well exercised and tested, much as is is a mature library. You need libraries anyway.
For what is worth, I believe that 99% of what you can express in one language you can in the other. If you're hitting walls with the borrow checker when writing Rust you always have the options of either using the equivalents of unique_ptr<T> and shared_ptr<T>, or of cloning memory.
> A Rust compiler bug could expose you to a memory fault, in the same way that a C++ library bug could; but the compiler is well exercised and tested, much as is is a mature library.
I'm not sure what this sentence was expressing, but it seems you're implying here that rustc isn't well exercised and tested?
[1]: https://medium.com/@lightcone/iterator-invalidation-in-moder...
It is hard for me to imagine how "the compiler is well exercised and tested" could be perceived to mean the opposite. Explain?
I misread it as talking about modern c++ compilers and libraries being well exercised and tested, and contrasting that with rustc.
You say that it's not "tempting" to use less-safe constructs in C++ these days, but that's not the point. You are assuming a perfect C++ developer who never makes mistakes and always knows to use the proper, modern constructs, and will properly document any time that they absolutely must use a less-safe construct, and that documentation will never drift or go out of date.
That developer does not exist in any meaningful or useful way. Developers make mistakes. Developers don't always have completely up-to-date knowledge or understanding of what the correct, "modern C++" way is to do literally everything. A developer who genuinely does need to do something less safe may or may not document it, and anyone who comes by later and changes the code may not update the documentation accordingly.
The Rust compiler will not allow you to do unsafe things (absent bugs in rustc); attempting to do so is a compiler error. If you really must do unsafe things, you must surround that code in an unsafe block, which serves as documentation that must be kept up to date, or the code will not compile. Others can inspect your code and very easily decide if they want to use it based on how much unsafe code they are willing to accept.
I don't want a compiler that will only ensure certain kinds of correctness if I already know how to write things in the "correct way" (which has traditionally been a moving target in the C++ world). I want a compiler that will ensure those types of correctness always, and reject programs where it can't give me that guarantee. The C++ compiler will not do that, but the Rust compiler will.
> In modern C++ you get compile-time correctness by construction
No you don't! You only get this if you've written your code correctly! The entire point of "correct by construction, verified by the compiler" is that you cannot write the code in an incorrect way that the compiler will accept. And the C++ compiler absolutely will accept "old-school C++" that could be incorrect. Put another way, there is no C++ compiler that will only accept "modern C++" and reject "old-school C++". And even if someone were to try to write such a thing (I am skeptical it's possible), reasonable, knowledgeable C++ developers could easily disagree on what should and shouldn't be included.
Fair enough. Yet, there are literally many thousands of fully competent, up to date C++ developers for each person who ever so much as compiled a hello.rs. (Probably the majority of the latter are among the former.) By 2030, under the absolute best-case scenario for Rust, there will still be literally millions of times as much C++ code as Rust code to maintain and improve upon.
> The Rust compiler will not allow you to do unsafe things ...[without] an unsafe block ...
In other words, the Rust compiler will allow you to do unsafe things with the addition of 8 characters. The temptation to that varies.
> The entire point of "correct by construction[...]" is that you cannot write the code in an incorrect way
The C++ language provides a comprehensive type system that enables the compiler, with well-engineered libraries, to prove correct use of types the libraries define. This depends on library designers taking up the mantle that the Rust compiler takes for itself. As they do.
But you need competently constructed libraries anyway, in Rust as much as in C++. Many demands are placed on library designers and implementers. Ensuring that only correct usage compiles (absent "unsafe") is not among the most difficult of those demands.
Rust will not save the world. That would be asking too much of it. There is no substitute for competence. Rust is not one.
C++ improves on a strict three-year cycle, and new C++ code can be demonstrably better, by any measure, than was possible for old code. To the degree that Rust and modern C++ can displace C, Java, and bad old C++ code, both can contribute to a better future world. Rust will not displace C++ in any plausible future, but not displacing C++ will not mean Rust has failed.
This is actually something I agree with, even as a fan of Rust. Sometimes I wonder to what extent the appeal of Rust is based in the fact that you get to feel like a CS undergrad again, climbing mountains to make code compile and run
I don't believe this, but I'd love to be wrong. Is there an exemplary codebase somewhere that I can take a look at?
struct Shape {
Shape() { init(); };
void init() { reset(); };
virtual void reset() = 0;
};
struct Point: Shape {
virtual void reset() { _x = _y = 0; }
double _x, _y;
};
int main() {
Point p; // KA-BOOM
}
Compiles without any warning with -Wall & -Wpedantic, fails at run.Clearly, this isn't it, but I do appreciate the interesting snippet. Would the compiler have caught this if `Shape()` called `reset()` directly?
struct Point {
double x{}, y{}; // zero-initialization
};
int main() {
Point p; // ok, {0.0, 0.0}
...
p = Point{}; // no need for a "reset"
}
It has been literal decades since anyone competent would have made a Point derived with virtuals, or have written so much code to achieve so little. (You might see stuff like that at Google.) You can write bad code in any language, but if you have to do extra work to make bad code, it is not tempting.I would argue that (i) old-fashioned code is not deliberately going out of your way to write bad code, and (ii) this should, at the very least, trigger a warning from the compiler.
Anytime you do all the extra work to write old-fashioned code, you have earned the outcome you get. The oldest-fashioned code looks just like C, which you can still write in a C++ program, if you want to. But there is no reasonable temptation to. Good modern code is equally fast, often faster, and more easily written, understood, and maintained.
The problem being that there is no definition of “modern” C++, and even less so that would be enforced by compilers.
> Anytime you do all the extra work
There is no extra work to write “old-fashioned” code. Quite the opposite actually; one has to indicate to compilers to accept newer features rather than the opposite.
> Good modern code
This is a very flimsy, handy-wavy concept, that changes drastically from one “best practice” guide to the other.
When people say this, they almost always mean making heavy use of C++11 (and later) features, defaulting to `unique_ptr` (or `shared_ptr` if it absolutely needs to be passed around), etc.
Also there's a sizeable minority of people who also mean staying on the stack whenever possible. Which, don't get me wrong, is a good thing, but that's been a thing going all the way back to C. I chalk that particular one up to those people having to debug C++ code written by java devs.
Besides value types' benefits to code comprehension, they often give the optimizer enormously more latitude to operate because it knows there are no stray pointers to the object.
Over-use of std::shared_ptr is called Java Disease. It has been seen to be curable. A value object containing just a std::unique_ptr<Impl> member (often called "Pimpl", pointer-to-implementation) gets most benefits of value semantics, in cases where Impl is bigger than you would want to pass around directly, and stylistically is overwhelmingly better than passing and returning std::unique_ptr.
If I were to try to pick it up again today, I would have to learn "modern C++"[0] incrementally. I would still have some old habits, and they would take time to iron out. My guess is that it would take me at least a year, possibly more, to become fully proficient in "modern C++".
And even then, I'd still expect that I'd occasionally write some C++ in the "old" way. Maybe I'm tired and forget, maybe I'm lazy and want a shortcut. Who knows. The compiler won't save me from my "old C++". It'll be there, warts and footguns and all.
I'd much rather write in a language designed to not have these problems in the first place, and let the compiler catch as many problems as it can.
[0] Whatever "modern C++" means; I suspect current C++ developers can reasonably disagree on the details, as has been the case for the entire history of C++.
Joke aside, the STL grew in complexity and features to provide more compile-time constructs and to integrate more functional programming aspects.
For example: I've been told many times that in "modern C++" you don't need `new` nor `delete`.
There are smart pointers, std::array, std::optional, const-expressions, you'll find more and more "single-header" libraries (for JSON, an either monad, etc...), even modules[1].
It's like learning a new language. C++11, 14, 17 and 20 are completely different to C++03 while remaining backward compatible (a critical feature for C/C++ languages it seems).
And even then, I think you're still behind rust. Sanitizers can only catch data races / memory errors / uninitialized reads / etc that actually happen. OTOH, Rust _proves_ that your program doesn't have these faults (modulo unsafe blocks).
As Dijkstra noted, testing can only prove a program wrong. The way to get correct programs, in any expressive-enough language, is by construction. At each level, do only operations that are well-defined by the level below. Expose only well-defined operations to the next level up. The compiler proves that the types are used correctly; so, when coding a library, you put the type system to work to make wrong code harder than correct code.
That is the right way to code Rust, too.
The best-defined software I know of is SQLite. Every function is carefully documented with a description of all failure modes and returned error codes.
But also... SQLite has one of the most comprehensive test suites in existence, so I guess they lost? https://www.sqlite.org/testing.html
SQLite are in a good position to switch to building with a C++ compiler, and then modernize incrementally. They won't, for cultural reasons. They probably could use that C-to-Rust translator thing that was used on Quake3, but they won't do that either, for better reasons.
You’re demanding a rigor that is not possible with a modern technology stack.
I am responding to this idea:
> If you are relying on testing for correctness, you have already lost. In any language. As Dijkstra noted, testing can only prove a program wrong. The way to get correct programs, in any expressive-enough language, is by construction. At each level, do only operations that are well-defined by the level below. Expose only well-defined operations to the next level up.
I believe this idea is naïve. CPUs contain undocumented instructions, and they expose implementation details via speculative execution and timing attacks.
So... by this metric, we have already lost.
I think I also take issue with the idea that there is a stable C++ dialect. GCC, Clang, and MSVC have always disagreed on how they interpret the C++ standard.
If you want portable code, there is no substitute for testing your program with every compiler you support and on every architecture you support. Proofs won't save you and the standard won't save you, because both proofs and the standard assume that we started from a bug-free foundation that doesn't actually exist.
You don't seem to realize that testing and proofs are just two faces of the same thing. A test is just an empirical proof of something. Both are rooted in the same logic and assumptions. In fact, in some cases, a test can be exhaustive and then it's as good as a proof. Tests usually have the disadvantage of being inexhaustive, and having to work their logic within the language.
I'm getting to the point that security software, such as an OS kernel, that is vulnerable to timing and speculative execution attacks, is that way not only in spite of being proven, but in spite of testing also.
It's not the case that proofs will fail to reveal that problem, but testing will.
No amount of testing will reveal a problem missed by proof methods, if both the testing and proving are rooted in the same false assumptions.
(Assumptions like "the hardware's protections mechanisms are sound, so that no data flows are observable to an unauthorized domain, so we just have to test the software itself is good.")
https://www.cs.utexas.edu/users/EWD/ewd11xx/EWD1162.PDF
In this paper, he begins with a program written in what I believe to be propositional logic notation. This program can't be typed into an HN comment, but it can be trivially translated in to Common Lisp:
(loop for k from 0 below n always (f k))
The rest of the paper is about him translating this program into another kind of math notation with C-like semantics. By the end of the paper, he has proven that the second, lower-level program is equivalent to the first program, which is the same thing that a compiler does.
He does not prove that the thing he originally wrote at the beginning correctly expresses his intentions. So his method is basically this:
1. Write a bug-free program in a very high-level programming language, or a very rigorous pseudocode.
2. Translate it to a low-level language.
3. Prove that the translation is equivalent to the original program.
Works in what way? Serious question. Do you mean "works" as in 1) "doesn't crash/seg/overflow" or 2) "doesn't UB" or 3) "doesn't have race bugs" or 4) "does what the programmer intended"?
I would say from my experience, none are strictly true. I'll hazard you mean to imply the dev knows the language pretty well in order to satisfy "works lvl 1" but you still have veterans running into bugs of the 2-4 variety.
But even to get to (1), C++ requires a ton of domain knowledge. I've been working with C++ intensively for several months and incidentally for years, and I still seg every now and then. Even after using Valgrind, it feels rickety. Meanwhile, I've spent a few dozen weekends on Rust and I just feel way more confidence that my program will "just work" without crashing.
That's all still "level 1 works". When it comes to race conditions and programmer intent making its way into correct code, it's no comparison. Rust is way easier to get my intent into a running program. C++, I still have to lean into logs and debugging, because it just doesn't quite do what I want a non-trivial amount of the time.
It's not a religion. Rust simply is a better experience. I can say that having learned both basically side-by-side.
It takes discipline to learn to write C++ in the modern way. It is work to keep up with improvements in the language and library, as new Standards are published. But many, many professionals do. C++ is not a toy. It has sharp edges that must be treated with respect. But if you do make the effort to keep up, and lean hard on the type system to help everywhere it can, the language delivers.
If your code feels rickety, it is. You have at hand the tools to fix that. If you find yourself coding races, it is because you have not adopted means to prevent them.
I can testify that coding in modern C++ can be pure fun. I never have occasion to worry about memory safety, or data races. Code not working the way (I thought) I wanted, on first run, is very much the exception.
If it is such a core just to hope to, one day, be able to code that, maybe, won't crash, what do you think is the point of using C++ over Rust?
In some possible futures, Rust code may take up just a bit of the load. The most value would come from its displacing C and Java in new projects.
Liiterally millions of times more benefit.
I'm not saying all the complexity of C++ is its own fault, but Rust shows how much of it is unnecessary. It manages without any constructors at all (think how many rules and features are connected to them!), without inheritance, and with only one (1!) way to initialize a variable.
Rust has some type-system complexity to get your head around. It's not as painful as it looks (when you're just reading it rather than trying to do it) but there is a hump of understanding there that you have to get over. Once you've done that though, Rust then helps you so much more in dealing with the 'long tail' of difficult problems.
Another way of looking at it is that there are a load of things that you can silently get wrong in C++. There are good practices that help you not screw up e.g. you have to learn how to think about who has pointers to what and how long they live. Rust forces you to think about that stuff from day 1 - the compiler requires you to prove to it that everything is good. It's a bit painful to start with, but it's a good thing overall.
Np language can solve the inability of writing a correct program. Do you believe if your Rust program compiles it is free of errors??
If you get one.
In Rust if I overrun the bounds of an array I will get a panic. It is deterministic and specified, and the stacktrace will tell me which array I overran.
In C/C++ I get no such guarantees - the behaviour is explicitly undefined. If you are LUCKY you'll get a segfault there and then. For one off the end of an array that's unlikely, particularly if it's on the stack. You're much more likely to silently corrupt some data. The program will probably eventually segfault out, but there's no guarantee it's anywhere near the cause, and it could have done anything in the meantime. If you're on embedded it's even worse - no segfaults there at all.
No, if my Rust program compiles it is not necessarily free from errors. It is almost certainly free from memory errors though. Memory errors are problematic, hard to debug and the largest cause of security holes.
Unless you write C++ like C with classes, which is not really writing C++ at all, Rust is actually simpler, easier to learn on your own, more consistent, with better compiler error messages, tooling, documentation, etc.
Your situation - boiling frog analogy - is a very common case but also the only one in which case C++ feels less complicated, simply because it's familiar.
Everything about C++ feels overwhelming, with so many decisions and degrees of freedom at every step, from build system, to lifetime management, mutable state, concurrency, and threading, to libraries, to dependencies, to deployment. It often feels easier, especially for beginners, because there are so many ways to silently do the wrong or suboptimal thing.
From my experience, it takes any new engineer, even if they have significant Rust experience, at least 3x the amount of time to get comfortable with a new Rust codebase than it does with a new C++ codebase. BUT, once you get past a certain stage of complexity, and everyone gets accustomed to the Rust codebase, managing the Rust codebase becomes a lot easier than the C++ one.
Some things only make sense if you know very old Rust, and they've just been updated as the language has been updated, obfuscating their original meaning.
For example let's say a security exploit surfaces in the 2015 edition of Rust, would that not mean all the libraries declared as 2015 edition would have to be updated or abandoned in that case?
Or now that I think about it, is it instead the case that a whole program including all dependencies will be compiled by the same compiler (of which newer editions will have the latest security fixes), just that the compiler will always have to support compiling programs using legacy syntax when it identifies the crate's edition?
Rust is not ABI-stable, there is no guarantee that you can even mix libs built with different versions of the compiler. The entire Rust tooling is built around static linking and building all your dependencies from sources. So yes, all the crates that go into your program are built with the same compiler, it's just that the compiler knows how to paper over the syntax differences in the different language editions.
It's this. Rust doesn't (yet) have a stable ABI for functions that aren't marked `extern "C"`. Any security vulnerability that would affect code in rust-lang/rust would most likely be in the standard library, which doesn't change between editions. All code links to the same libstd. Only the compiler frontend changes
[0]: https://github.com/rust-lang/rust/blob/69b352ef7749825abde2d...
[1]: https://github.com/rust-lang/rust/blob/69b352ef7749825abde2d...
Here are another couple of ways of writing the same general concept:
f({
return;
() // () in this case because f takes a parameter of that type
});
let x = return;
f(x);
So f() will never actually be called. You’ll get an unreachable_code warning on f.As a minimal case it’s mostly just funny, but the underlying concept does find its way into real Rust code, mostly in generated code (such as with macros). Also, although just about everything’s supposed to be an expression, sometimes things that can be an expression or a statement go a little bit funny, and return is a prime candidate for things going wrong in weird ways in the compiler frontend or in code generation or something, so it’s worthwhile having such tests (there will be more involved tests of return specifically elsewhere).
{
let x = return;
f(x)
}
where the call to f is clearly happening after an unconditional return.A slightly more practical example might be something like
f(if blah() { 42 } else { return })
which works out to, roughly, {
let x;
if blah() {
x = 42;
} else {
return;
}
f(x)
}
To get realistic, if you write f(g()?)
you basically get f(match g() {
Ok(x) => x,
Err(e) => return Err(e),
})
which is how rust does error propagation without exceptions and without constantly repeating a golang-style mantra. loop {
// …
state = match state {
A => B, // normal
B => break, // exits the loop
C => { …; continue; }, // does something and then returns to the start of the loop, skipping setting the state and anything else
D => return, // exits the whole thing
E => f()?, // if f() returns an Err or None or similar, it’ll return early
};
// …
}They’re always expressions in Rust, though they are often written with ; to make an expression statement.
Basically, the only thing that is a statement and can’t be an expression is `let pattern = expression;`.
if some_bool && let pat = expr {}There is also #![]. The difference is what they apply to, #[] applies to the following thing, #![] applies to the parent thing. You'll see #![] to enable nightly features in Rust, for example, and #[] for things like custom derives.