Of course.
> Coding C++, you need only choose known-correct primitives to get the same level of assurance.
C++ does not even have the means to express "known-correct" primitives. The compiler will not tell you if you accidentally access a non-thread-safe type from another thread. It will not tell you if you accidentally resize a vector while having an outstanding reference to one of its elements. These are not "bad primitives" you can blame - getting this stuff right requires higher-level reasoning which the C++ compiler cannot do for you, and the Rust compiler can.
> is exactly as safe as Rust eBPF, but likely less annoying to code.
Personally I find C++ far more annoying to code. I mean header files... In 2022? Text based macros? No standardised way to manage dependencies?
Yep. This is basically why I switched to Rust from C++, but I stayed for the coherent language design, the rapid pace of improvement, and the borrow checker.
It is an observable and downright undeniable fact that developers, even expert developers, cannot consistently write correct C++ code. This is true even if you have a greenfield project that uses all of the modern best practices and you don't have the keyword "new" anywhere in your codebase. People fuck up lifetimes. People fuck up iterators. People fuck up integer arithmetic. People fuck up concurrency. Using a subset of the language and relying on modern tooling like sanitizers will not save you.
It has been literally years since I shipped a memory usage bug. It just doesn't come up. There is no temptation to make memory usage bugs, because they would be extra work to code.
I understand that there is a sub-literature devoted to making up stories about how the C++ experience is exactly identical to C's, by people insisting they are active, competent C++ coders, but there are almost always clues that this is false.
I also spoke about way more than memory errors. C++ has a million other footguns that people regularly fire beyond accessing deallocated memory or accessing memory out of bounds of an allocation.
I have written c++ for 28 years and I can only recall letting something rust would have protected me from make it out of test once. That I know of...
A coworker found some UB in a rather large c++98 codebase recently. It wasn't exactly idiomatic modern c++. But the same person writing the same code in c++20 would just have had explicit and auto in more places. It was a use after free on the stack that wasn't terribly subtle but I only found it because I knew something was there. Cppcheck and clang-tidy were no help. Debug builds didn't exhibit the UB with g++ or clang, clang didn't exhibit it with -O3 (optimized out), but g++ did. Asan would have caught it, if they used asan (they don't) and if they had unit test coverage for that branch (also no).
So experienced c++ developers with years of experience (no novices on that codebase) made an error that can't be caught by static analysis, and requires 100% branch coverage for asan to catch. It eventually popped up in production.
I find modern c++ much more pleasant to write than rust. I like that it is easy to interface with the rest of the world.
I hate spending eons to get unit tests to find that last path theough the code in a meaningful way. I hate waiting for pipelines to run unit tests and coverage to find a mistake.
Modern C++ built with static analysis (including modernization checks) takes as long as a rust builds, but still doesn't catch as much stuff.
I'm not using rust yet, but I'm planning to start. I'm tired of this stuff, it's been almost 30 years.
> I can easily write something that doesn't use traits or iterators, panics all the time, and calls clone all over the place to satiate the BC and call it a day right?
You're basically not going to write your own traits until you've spent the time required to get something working. You'll copy and paste and modify to create another function.
If you're unfamiliar with iterators, then you'll write for loops. I was unfamiliar with iterators until I said, "I'm going to rewrite every for loop in this personal project as an iterator." Rust forces you to use lots of patterns re: memory, but forcing you locally to implement as a for loop or a iterator is not one of them.
I think a fear of clone() is wrongheaded. Most of the time it just doesn't matter.
When you think, ugh, this isn't the best I can do, it's time to learn iterators/traits, then you can modify the code to work then.
> But with all those features surely I'm missing as many pitfalls as I know about?
I guess my question would be: compared to what? I think the pitfalls are much, much, much more explicit in Rust, than C, like this won't even compile because this code has a condition which might be a null which I have not handled.
For app development, Rust really feels like a smallish language once you get over the hump.
Do you mean that you aren't using them at all or you aren't defining your own? Because I would expect any nontrivial rust program (outside of some obscure embedded context maybe) to make use of the traits that are in the standard library/your dependencies. Similarly, you can't even write a for loop without using iterators.
>panics all the time
That might be fine, depending on your usecase. Presumably if you were going to put it into production you would go through and handle the errors. Crucially, by grepping for `unwrap` and `expect` you can find the panics very easily.
>calls clone all over the place to satiate the BC
If it's not a performance issue then this might be fine.
>I could create very unidiomatic code that was still correct, but not performant or easy to read/use
This is true in every language. The value proposition of rust is that if you do write idiomatic code then it will likely be reasonably performant and easy to maintain.
>But with all those features surely I'm missing as many pitfalls as I know about?
Maybe, but not every program will need to use every feature. Quite a few of the more advanced features are mainly useful for libraries, for example.
I write Rust code up and down the stack, and this is the thing that ticks me off more than anything else. You can get most of them looking for this stuff (and a handful of others), but there are just so many places in vanilla Rust that exhibit the Wrong Behavior By Default, e.g.
a[i] = x + 1;
Which looks so innocent but can panic in two places! Ugh.For example if x is Wrapping<u8> then it can't panic. Having chosen Wrapping<u8> you will presumably not be startled when 255 + 1 == 0
Or if x is Saturating<i16> again, no panic, but 32767 + 1 == 32767
Wrapping and Saturating really exist (well Saturating isn't stable, but it's in nightly) and if you're in a space where that's what you want, you can express it cleanly in Rust. Most programmers just mean the integers they learned in school which don't have a "maximum" or "minimum" they go on forever, and so overflow means the programmer's model of the world mapping those integers to a type like usize was wrong and this code is faulty.
[Edited to add: This doesn't quite work because in the end neither Wrapping nor Saturating implement Add over the normal integers, since it's not certain what you intend if you wrote that, better to have an error asking you to explain. AddAssign works though]
Now, there aren't any built in types whose IndexMut can't panic AFAIK, but we could build one, and if a was such a type then a[i] doesn't panic either.
For example we can imagine a is a (hypothetical) PythonDict while x is a Saturating<i16> and now our code just compiles to add 1 to x, saturating if necessary, and then shove the result in our PythonDict under the value of i.
Since indexing is nearly always fallible, it should be fallible!
I'm confident we sometimes want each of the three options, so if we're having the Index operator return None then get() presumably panics, or at least some new function is provided with that behaviour.
I reckon this just drives annoyance that [k] has to be unwrapped, while get(k) does what people actually wanted [k] to do. I could be wrong, but that's my sense.
If the index operator is offered as sugar over one of the basic operations, great. But why add sugar for a method call that we actually don't ever want to see in production code?
- indexing_slicing
- integer_arithmetic
Enabling those will throw an error on usage in your codebase.
Also it's worth noting that arithmetic is only checked against overflow when building in debug mode by default.
Yes, the default behavior in release mode is even worse!
"Fighting the borrow checker" has become a Rust meme but it's actually a good part of the Rust experience to have a "conversation" with the compiler. One is forced to reflect upon the implications of the app or lib's model, often leading to a better design or at least an understanding of the limits of the code. This is possibly similar to what LISP people describe when talking about their REPL-based workflow.
> Coding C++, you need only choose known-correct primitives to get the same level of assurance.
C++ has no such primitives that offer the same level of assurance as Rust does. Not every disagreement is spiteful.
> Coding C++, you need only choose known-correct primitives to get the same level of assurance.