> 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.
> 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.
It's definitely worth reading the full thread if you haven't
> 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".
I disagree. There's a clippy lint against pattern matching on bool. I also think Result has the connotation of "success or error". Plus it's always annoying when a function returns an error that doesn't implement Error and I have to wrap/replace it to make it integrate with the standard error handling ecosystem.
There's a nice alternative though, which is empty struct errors.
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.
I like matching on bools on the same cases where someone wouldn't write braces around the body in C: whenever the corresponding expressions keep you under the line length. For example:
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),
};
Hang on aren't we conflating two things in this thread? Or you quoted the wrong half of GP's comment perhaps? You can pattern match on an actual `bool`; if you use `Result<(), ()>` instead you'll have to match on `Ok(_)`/`Err(_)`, which is bad because one of your Boolean values is an 'error' now, and also because which one is is up to you (to remember)!
Similar debates exist in all programming languages with algebraic data types. `Either<(), a>` vs. `Maybe a` and stuff. Category-theoretically, they are the same of course. But in non-strict languages `Either<(), a>` allows another opportunity to hide an `undefined`: `Left undefined`.
I find it beautiful that HashSet<T> in Rust is a thin wrapper around HashMap<T, ()>. I'm glad it's a wrapper, though. I wouldn't like it if everyone had to write `HashMap<u16, ()>` and so on and know which methods still made sense.
> `return x` is an expression that can be used in place of any type because it will never be used.
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.
Whaat, was this new in 2018 ed? I could've sworn it was `()` and so you couldn't do that. I don't write rust that often, and happen to be reading this just as I've written some code that's.. not that messy but not that I'm pleased with, wrangling around exactly this sort of if-assign-else-return pattern.
I don’t believe it should be new, but this can happen by accident sometimes. Like, if you included a ; on the non-return branch it would return () overall and you probably thought the return did it when it wasn’t that. Hard to say!
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.
Oh gosh - I was so primed by the poem to see <woe, is> as a generic specifier (which I thought should fail, because "oh" isn't callable)... that I completely forgot that < is a less than operator and > is a greater than operator. This literally compiles as two string comparisons. I need to go home and rethink my life.
The grammar conspires against your brain; when used as comparisons, we tend to include spaces around them, and as generics, we don't. But it's not required for either of those to be that way, with the whitespace. Tricky!
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.. :)
"Another lesson we should have learned from the recent past is that the development of richer or more powerful programming languages was a mistake in the sense that these baroque monstrosities, these conglomerations of idiosyncrasies, are really unmanageable, both mechanically and mentally. I see a great future for very systematic and very modest programming languages."
I mean the fact that you can do funny stuff with the syntax rules and the type system doesn't disqualify their usefulness. Rust's type system is impressive as it enables enforcing powerful constraints over how the code is allowed to execute, eliminating whole classes of bugs, and enabling elegant, zero-cost abstraction over swathes of algorithms and data structures, which in turn lets you have high quality libraries that provide to-the-point solutions for so many of the common tasks that a developer will deal with day to day.
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.
The problem is that the complexity in computing is intrinsically irreducible. Sure, you can get nice nifty little languages, but then the latent complexity will be transferred into the program itself (e.g. program length, cyclomatic complexity, etc.) rather than in the language; that's why although they can technically do the same thing, we have a spectrum of languages ranging from assembly to APL.
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.
"I see a great future for very systematic and very modest programming languages."
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.
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 ...
One thing that people say is that "Rust frontloads your problems". C++ might be a bit easier to get your head around to start with, but as the complexity of what you're building grows you will discover the 'joy' of debugging segfaults and race-conditions.
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.
having segfault is the best you can have: it tells you explicitly that something is wrong with your program. in other languages the same kind of error may result in stale data and the GC does not collect the data and the memory consumption grows slowly. You will notice the problem much later.
Np language can solve the inability of writing a correct program. Do you believe if your Rust program compiles it is free of errors??
> having segfault is the best you can have: it tells you explicitly that something is wrong with your program.
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.
This is not the same kind of complexity. Rust mostly reflects complexity of its domain (thread-safe, memory-safe, low-level without runtime, a lot of correctness enforced at compile-time), rather than unfixable baggage of C and C++'s early design decisions.
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.
I learned in this order: C, "C++" (very C-like, just enough to write arduino stuff), rust, and the "real" C++14.
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.
I've written tons of Rust and C++ over the last 6 years, both in the context of large, complex codebases with hard real-time performance requirements. I've also managed teams where I've had to 'train' new engineers and I can say that it is categorically false that Rust is easier to learn than C++, it's not even close.
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.
I often feel the same, and there is certainly a bit of learning curve, but in my experience, all the time you spend battling the compiler in Rust is time saved from debugging in C++, several times over. I'm repeatedly surprised by how my code just works when it finally compiles. It's like the compiler is telling you, "you just made a bug".
Just to give an alternate viewpoint, I'm currently writing most of my code in Rust, and while I am generally a fan of the language, and a huge fan of the tooling and community, I think Rust often gets a pass on complexity because it's mostly compared against C++ which is itself a monstrosity. There are many, many other languages to which Rust seems insanely complex and difficult to program in by comparison. I think that often gets blamed on accommodating the borrow checker, and Rust's low-level nature, but I think a lot of it is avoidable.
I'm curious, not saying you're right or wrong: what bits do you think are avoidable?
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.
Cool thanks! This is a great example of my mental bias, actually: I don't tend to think of the module system as a "language feature" even though it clearly is!
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 definitely think you're right that the module system is complicated, even the new module system. I would like to make one nit though: it's definitely deterministic. Deterministic just means it can be predicted, not that you specifically can predict it. If the module system was non-deterministic that would be a much bigger issue
Ok fair enough, that is true. I guess it would be more accurate to say that a module declaration is ambiguous with respect to the file-system location of the code it's referring to.
I'll chime in and say that for me at least, the module system was a hurdle at the start and coincidentally the only part of the language where the explanation in The Book (which is excellent, so thank you, btw!) did not click for me.
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.)
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.
> 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.
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.
The rules are mainly there to filter out emoji; it can do most non-ASCII outside of that. L͇̭̝̲͍̠̼͒̌̋̄͟͝ͅi̢̭̬̥͍̣͓͗͆͆̄͝k̸̙̹̠̘̤̪͓̼̠̋̽̿̇̕͜͝͠ė̜͙̥̟͍͇͉͖͌́̑̈́̎͐ Ž̶̨̛͇͙̹͖̳̂͊͐̅̇̓a̸̭͙͇͙͙̝̟̯̮̠͌̊̑̏͊͡ĺ̸̻̟̩̼̙͈̒̿̀̈̓̌́̐̓ģ̵͉̺̹̥̤͎̟̓̓̑̋̈́̇͢͜͡ơ̷͖̤͎̮͈͆̌̿͂̂̚͘.̴̱̬̮̟͖͆̄̉̔̏͛̒̇̌ͅ
I don't know what "modern communication" you're using, but the only emoji I use are ones that can be written in simple ascii, :( , :-/ , :) , :D , ;) , etc. Most emoji are simply symbols for different activities, but they don't need to be used at all. The only place I see heavy use of emoji is on twitter, but that's because of the character limitation. Additionally emoji are colored and not black and white like text so they're glaring with respect to text around them.
You might be interested in taking a look at and potentially participating in the "Async Vision Document"[1] which is an exercise the team is going through to collect feedback about the current state of the ecosystem and what the pain points are, as well as a way to lay doing what the desired future state of async Rust should be[2]. The process is happening, as you would expect, in the open and there's still time to influence it[3] if your concerns aren't yet addressed or even mentioned[4].
> 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.
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.
> builder chains are a gigantic misfeature to paper over the fact that the language doesn't have default/named function arguments.
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.
After your battle with the compiler is over and it compiles it does not mean your program is correct! If you now what you you have no issues with C++. It's not the language. It's the design, approach and attitude
If you ever find yourself spending time "debugging in C++", you are Doing It Wrong. Coding modern C++ right is the same, that way, as coding Rust: when it compiles, it works.
(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.)
> Coding modern C++ right is the same, that way, as coding Rust: when it compiles, it works.
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.
> 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 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?
C++ doesn't need a point. C++ is a mature language with mature tooling, an installed base of tens or hundreds of billions of lines of code, millions of professionals worldwide getting critical work done every day, with guaranteed prospects decades into the future. You personally depend on C++ code not crashing hundreds or thousands of times every day, without knowing it.
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.
I depend on a mountain of C and C++ code to not only not crash, but to also not have any subtle memory errors which might allow a packet from the internet to run arbitrary code on my machines. Failures of that sort are disturbingly common.
You stand to gain more by people modernizing their existing code -- even, compiling their C code with a C++ compiler, and then improving that, as happened with excellent results in gcc and gdb -- than by hoping a vanishing few will switch over to a wholly new, unfamiliar language.
The only way this is even close to true, imo, is you have really good tests and have taken the time to get those tests running with every available sanitizer, including MemorySanitizer (which is a huge pain).
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).
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. 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.
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.
You do know that every operating system and CPU in existence has ill-defined operations, right?
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.
SQLite is coded in C. Correct-by-construction was not among the choices available.
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.
In a greenfield project, you can restrict the code base to stable C++ dialect (all the way back to C++98 if necessary), and you can dictate the use of C++ features which make it safer than C. For instance, smart pointers for all memory allocation.
> 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.
If you use undocumented instructions, you're not following "do only operations that are well-defined by the level below". The existence of undocumented instructions doesn't invalidate the concept of correctness by contract.
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.")
Erratum: testing, of course, exercises every layer you depend on, whereas proof methods typically assume that the things below conform to their contracts. That is where proofs are weak. A proof will show that your code is correct; a test will reveal a compiler bug, CPU bug, faulty RAM, ...
> 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.
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
Thank you, this is an example of '90s-style C++, serving as a nice contrast to modern C++. Nowadays we would write
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.
Old-fashioned code is not necessarily bad code, although the example was; but the topic was "modern C++", not "old-fashioned C++".
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.
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++.
Modern C++ is what was in the Boost framework 15 years ago.</troll>
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).
> but the topic was "modern C++", not "old-fashioned C++".
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.
> The problem being that there is no definition of “modern” C++
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.
ByteJockey refers to value types, and value semantics.
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.
You can compare them, but the comparison won't show that they're equal :p
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.
There is nothing similar between Rust's borrow checker and anything in C++.
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.
Genuine question, is iterator invalidation still a problem that people can hit with modern C++ constructs? A quick search seems to answer "yes"[1], but I'm not involved in writing C++ to evaluate what the common mitigations are.
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?
Iterator invalidation, like pointer invalidation, remains a thing. Iterators and pointers are normally treated as ephemeral, except where guarantees are provided.
It is hard for me to imagine how "the compiler is well exercised and tested" could be perceived to mean the opposite. Explain?
Thats fine until "that guy" grabs the raw pointer out of a smart pointer and deletes it, or c style casts an interface to his version of the interface because he wanted it to return an int instead of a double. Then either pushes straight to dev or rounds up similarly evil individuals to approve the merge. I have debugged those errors. You can talk about accountability but even if you manage to get rid of one such dev there always seem to be more. And modern C++ doesn't really have great tools for thread safety. Its easier to get right than in 98, but still not hard to get wrong.
I think you're fundamentally misunderstanding the objections people have to "modern C++" when compared to Rust.
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.
> Developers make mistakes. Developers don't always have completely up-to-date knowledge...
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.
While I agree that Rust is complicated, this example from the test suite is really a bad example because it's there to test the edge cases of the grammar to ensure backward compatibility.
If you've been working with C++ for over 27 years, you owe it to yourself to at least try Rust. A lot of the features in it are designed to ergonomically operate around the limitations of compiled languages, while also encouraging the developer to write "correct" code. If you've got some spare time, I wholeheartedly encourage giving it a try!
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.
And I guess it’s 2015 edition, because it’ll fail in 2018 edition Rust: where the 2015 edition has ::u8(0u8), the 2018 edition will need you to write crate::u8(0u8) or self::u8(0u8), since only crates exist at the top level now, rather than the contents of the current crate as well.
Yeah it's indeed the 2015 edition because while compiletest watches for // edition:something comments [0] (like this [1]), it doesn't pass any edition flag to the compiler if no such comment is present, and the rust compiler defaults to the 2015 edition if none was specified.
However, the compiler has to continue to be able to compile 2015 edition code, as it is still guaranteed to work and be intermixable with 2018 (and soon, 2021) code in the same application.
For curious people unfamiliar with the situation with this: Rust cares about backwards-compatibility so that you should still be able to compile code from 2015 in 2042 (some libraries are just stable!), but it also wants to allow certain changes to the language that you might think would be backwards-incompatible. Each crate (library) declares an edition, and the compiler follows the rules of that edition, and then you can link all the different crates together without worrying what edition they were written in. Editions are limited in what they can change, because things like traits are shared between crates, so they can’t be changed in editions; it’s mostly syntactic stuff, like the 2018 edition making async/await keywords and making dyn a full (rather than conditional) keyword.
I'm not very knowledgeable with how compilers or language standards work, but would there not be security implications with this approach?
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?
It's just syntax differences. The newer compiler supports all previous language editions, you're not using a 2015-era compiler to compile 2015 edition code.
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.
> 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)
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
JavaScript had something close, valid on the context level, with "use strict" (which is, I guess, borrowed from Perl), and I still don't understand why they don't repeat that for newer features that would be much simpler if they broke backwards compatibility.
Well, the ability to specify python version by module would've made migration much easier for everyone (in theory). But you're quite right that it wouldn't by itself be a solution -- it would also have required additional complexity to handle interoperability when calling between python versions, both in the runtime and the programs themselves (even a sufficiently smart compiler can't figure out what string encoding a python2 function expects).
I've seen those sorts of things in other languages, though they're often accompanied by explanatory comments, if not a separate document. Given that these appear to be in a test suite for Rust itself, I'm surprised that these aren't explained in the code (at least as far as I saw)
Good god. Wicked, bad, naughty Rust. Oh, it is a naughty language, and it must pay the penalty -- and here in Castle Congruence, we have but one punishment for setting alight the grail-shaped semantics.
So, semantically, what is `f(return)`? Are these examples supposed to actually do anything, or mirror actual code situations? Or is it just putting a monkey in front of a typewriter?
In Rust, just about everything’s an expression. `return` is an expression that diverges, meaning it’s of type never (spelled !, and coercible to any type, since you know the value will never actually be used).
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).
Building on the more practical example, this sort of conditional early return is extremely common in Rust, because expression-orientation makes it very pleasant to use. Here’s a sketch of some of the sorts of ways it can happen in state machine:
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
};
// …
}
Languages like this: fun to author the grammar, and have the entire thing in your head. So expressive! Difficult for anyone else (including yourself in 2 years) to understand
Virtually any language can be "like this," these examples are all nonsense designed to stress the compiler. This code isn't supposed to be understood by any human.
I like the idea, but I think Rust is too picky of a language to use for something like that. IOCCC is fun because of how interesting it is to watch seasoned C professionals finesse their favorite language. A Rust variant would likely end in tears, copious compiler warnings and perhaps blood.
It is called an "attribute" and it is part of the language itself.
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.
Yeah... I didn't want to admit that for a really long time, but Rust isn't really a huge success. You can obfuscate the code in most (all?) of the languages, but the fact that all of these things are even possible, definitely isn't something to be proud of.
> 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.