If it is so bad, it should (in the sense of how things would be in an ideal world) not compile.
>It is not, then, a closure at all.
It is a closure because all variables are closed-over and there are no free variables in the lambda body anymore. That is the definition of closure.
>But using a wrong function and getting wrong results is not surprising.
In an ideal world, there should be a compilation error. (There is in Rust)
The majority of what's wrong in C++ is that it lets you do nonsensical (even dangerous) things, most of the time without even a warning (and not because it's technically impossible to warn--it just didn't occur to them). It's okay to acknowledge that--it's a product of its time.
>Returning that fake closure should evoke a compiler warning, if you turn on warnings.
That "should" tells me all I need to know. In the end either safety is important, or it isn't. Choose accordingly.
Yes, C++ can be a bad solution to a lot of problems, and that's okay. Use rust if you need a machine guarantee for memory safety (or you just like the language), you can use Go if you just don't care about that at all and want the language to take care of it. But you can use C++ for non-critical software that needs to be fast (games come to mind). Rust can be too much of a mental overhead than it's worth for some.
That closure is just not a good example. Nobody would write this, because when writing C++ code you _do_ think about whether you want a reference or not. Sure, a lot of bugs can happen but this is not, in my opinion, one of them.
If you want to prove a point, prove it fairly.
Note: I don't use C++ anymore, and I don't like it very much for other reasons.
Rust has the least mental overhead of any language I've ever used. The compiler literally takes all the mental overhead away.
Increasing the strictness of the language has one effect: decreasing the number of potential solutions for a given problem.
If the restrictions are carefully chosen (like they are in rust) this leads to generally safer code. But don't fool yourself, the restrictions don't merely generate new solutions - they reject the ones that don't pass the tests.
A more extreme example is formal theorem provers. Carefully constructed proofs will take a lot of effort, but it will also make you confident that the code does what it needs to do.
The rust borrow checker is just a more restricted theorem prover that doesn't touch the logic, it just deals with the memory side of things. It's indeed very helpful in trying to explain what's wrong (and even suggest fixes), but it doesn't take the programmer overhead away.
In a more complex system rust inevitably makes it harder to come up with solutions. It will reject valid code just because it can't prove it's right, not because it's actually wrong. As a programmer, you're going to come up with such solutions, and while in time you get more used to writing code that rust likes, and rust too gets better at accepting correct solutions, you're going to have to fight the borrow checker sometimes.
I don't have a ton of experience with rust, but I encountered cases where equivalent C++ code would've worked just fine, but I had to change it because rust didn't like it.
Rust is an amazing language, but it definitely doesn't 'take all the mental overhead away'.
Not to disagree with your post but this comes across as very black-and-white.
There's a prioritized list of requirements in engineering. Use the right tool for the job.
Rust has: (see https://hackmd.io/@rust-ctcft/r1plN4You#/5 )
1. Safety
If you don't need that at the first slot, then the biggest strength of Rust doesn't apply to your problem (and you would waste time having to track lifetime parameters more than necessary to solve your problem).
The .clone() would have been [=] in C++, which would have safely quieted the warning.
To me, with the 20 years of experience I have in 8 programming languages, I'll always include safety. Especially having in mind that it's nearly zero-cost in terms of runtime performance.
So to me not choosing safety is a strong sign that I don't want to work with the people who practice that.
So, safety, good, fast enough, good, but insufficiently expressive? No, thank you.
Want to elaborate on that? Quite curious.
#[derive(Debug, PartialEq, Eq)]
pub struct ColumnVector<F, const DIM: usize>([F; DIM]);
impl<F: Field, const DIM: usize> Add<Self> for ColumnVector<F, DIM> {
type Output = ColumnVector<F, DIM>;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.0.zip(rhs.0).map(|(a,b)| a + b))
}
}
Usage: let v = ColumnVector::new([1.0,
3.0,
4.0]);
let w = ColumnVector::new([1.0,
0.0,
4.0]);
assert_eq!(v + w, ColumnVector::new([2.0,
3.0,
8.0]));I propose you start with genuinely concerning problems. Operator overloading is a first-world problem and I've made a good career never depending on it (outside of my active C/C++ years at the start of it).
"Generics argument concepts" says exactly nothing, to me at least. Elaborate?
"Programmed move semantics" is, if I understand you correctly, a flavor / taste thing but I can agree it can be made better and more explicit -- say by not using the `=` operator for it. That I could stand behind. Still, it's only catching you off-guard while you're learning. 50/50 though. It's a concern but IMO not a major one.
And your final sentence betrays bias to C++. Well fine, use that, nobody is forcing you to work with Rust, right?
But if you're willing to bash Rust, please do so with concrete arguments. If you know something negative about it that I don't, I believe I and many others will benefit from informed objective criticism.
Do you have that? Already asked you in another comment and I'm still willing to listen to it.
std::vector<int> suffix{3, 4};
auto append34 = make_appender(suffix); // Version 1: test will work
auto append34 = make_appender({3, 4}); // Version 2: test will fail
I was in a mood to type the examples but I accidentally made Version 1 because I had started with the author's first example. I wondered where the problem was and couldn't find any (neither running nor reading the code) until I noticed the author had changed to Version 2.The problem is "obvious" in hindsight but one of the problems with C++ is that it makes certain things too implicit/convenient. I get that references are a staple feature in C++ but it's not at all obvious from the calling location that Version 2 is a bug. To see that, you have to navigate to the callee (finding which might be hard enough alone without a very solid IDE), and make sure that it's not capturing the argument by reference or not doing anything unsafe there.
It's often a problem when a seemingly "value" argument is turned into actually a pointer-to argument at the callee. There are other languages that have this too, and I've never liked it.
As someone who doesn't regularly code in C++ but has a solid understanding of the basics, I wonder why C++ ever allowed to have a reference parameter be called with a temporary? To me it feels like "References have value syntax but pointer semantics BUT you should program like it had value semantics"? Which to me would be exactly a premature optimization that is looking for trouble.
If you want copy in your C++ lambda, you start it `[=](...) {...}`, if you want take a pointer in your C++ lambda, you start it `[&](...) {...}`, if you want something trickier you do trickier stuff.
Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe`, in C++ you have to opt in with e.g. `clang-tidy` or some annotations.
They are remarkably similar, just with different defaults.
I'm going to lead with this, because I think it's most important: Culturally there's a world of difference. Safety is a part of Rust's culture. "Culture eats strategy for breakfast".
Take sorting. In C++ the default sort is unstable, while in Rust the default sort is stable, that's just those defaults you mentioned (each has both kinds), although the choice speaks to culture. But look closer, in C++ the sort has undefined behaviour if your type isn't totally ordered. In Rust you can't sort the partially ordered types without saying how to order them fully. Still, in both languages we can write a custom order, so what happens then? In C++ if your custom order is nonsense you get... undefined behaviour. In Rust sorting won't necessarily work with a nonsense custom order but the behaviour remains well defined.
> Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe`
Unsafe gives you a small number of dangerous "super powers" needed to write efficient low-level code, it does not opt out of the borrow checker's analysis, or indeed most other checks. This misconception makes me wonder how much of what you've written is conjecture rather than practical experience.
Well in Rust the default `HashMap` uses a cryptographic hash, and you see it everywhere, it's the de facto community "default". In C++ the community "default" is `absl::flat_hash_map`/`folly::F14`, which use SIMD to compare a whole stripe of key-prefixes simultaneously.
I want different defaults for different programs, but the idea that it's esoteric to ever want an associative container within arms reach that fucking demolishes the other one is, ugh, God I want to like Rust even more than I do but this "we're right and everyone else hasn't seen the light" routine is infuriating and pushes me at least away.
My parent comment is trying to emphasize that this isn't a right/wrong thing, different tools for different jobs. And people are just like: "nope, everything but Rust is wrong".
However, I’ve never gotten the sense that Rust itself promotes the idea that “everything except for Rust is wrong.” I also don’t read much on the Internet these days, and I’m not doing so, probably avoid much of the hype that people are pushing about Rust.
Since it has been established as Hot New Thing, there are huge social incentives tied up in promoting it.
Having the "right" defaults is better for everyone. Folks who don't know or care get a good, safe default with no undefined behavior or unexpected danger, and folks who know better can opt into something that fits their needs explicitly.
Seems ideal to me.
Whoops - that should have said "a language with better defaults"...
Just to be clear, the Rust HashMap does the same thing.
I don't know how I missed this, is the Swiss port a fairly recent development?
The port is a little older, 2018. The idea was famously explained at CppCon 2017, I don't know whether Google had published on Swiss Tables before that year.
So, the trade is just: slow for nothing.
Such algorithms are crucial to avoid being subject to a Denial of Service attack which is, in fact, a security problem. Of course under the C++ "blame the programmer" philosophy you don't deserve protection from Denial of Service unless you knew you needed that and figured out how to ask for it properly.
Just as with the sort functions this is about safe defaults, not about constraining people who know what they're doing. Dropping in FNV instead of SipHash, or even using the identity function as a "hash", is not difficult if you are sure that's what you need.
But to the degree that C++ has an RTFM vibe, and I really don't think you'll hear Andrei or Meyers or Sutter talking that way much, it's uniformly applied and not particularly partisan. In my experience C++ pros would rather be writing Haskell and that's where you get all these over-templated "header only" libraries.
Rust is in a glass house on "blame the programmer" stuff, because it's "blame the programmer for being dumb enough to not be using Rust exclusively".
The memory safety catechism makes sense for my SSH client, or web browser, or web server, or shell, or another few dozen security critical things. And frankly I'd feel safer if someone did a ground-up reimplementation of `bash` in Rust, I'd use it in a heartbeat. Tailscale writes their shit in Go for a reason, you don't want even the possibility of a use-after-free in your VPN.
But most of the software I run? It runs as me, and if someone is running as me behind the firewall, I'm in deep shit already.
`rustup` has `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` on the fucking home page, ditto vim/emacs extensions, etc. I'm calling bullshit on the every damned thing needs to be DoS or timing-attack hardened. It's marketing, and my original point is that all the other great modern features of Rust make a much better list of talking points.
But that isn't the claim. Rust's defaults are safe. Remember Rust's one line description "A language empowering everyone to build reliable and efficient software".
This is like with the decision that Rust's sort() is a stable sort. I know what a stable sort is, and so do you, so if we care we may decide it's appropriate to use the unstable sort which could be faster. But programmers who don't know what a stable sort is aren't expected to learn about it before their sort does what they expected.
Same here, I know that SipHash is slower than Fowler–Noll–Vo, which in turn is slower than the identity function, and I know why it would or would not be OK to choose them, and presumably you do too. So if we care we may choose a different hasher for our HashMap. But programmers who don't know about hash algorithms aren't expected to go learn all this stuff before using HashMap.
I think maybe C++ isn't programming it's actually a live action "Um, actually" game where the stakes are your program arbitrarily misbehaves unless you correctly guessed all the things wrong with whatever code you just wrote despite the compiler insisting there's nothing wrong with it as written.
Could I do OK at that game? I'd like to think so. Do I want to play? No thanks.
It doesn't help that those of us that care about enabling the right defaults are a tiny minority as per C++ surveys.
https://blog.jetbrains.com/clion/2021/07/cpp-ecosystem-in-20...
I think the point the author tries to make is that, while C++ and Rust are probably "the same" for the most skillful and disciplined programmers (such as you), for average human, Rust just catches way more errors they make for them early. An extreme analogy would be trying to climb Everest all by yourself vs with a professional guide.
I suspect that the adoption of Rust as a system language is going slower because that's just the natural pace of embedded/systems development, not because of anything intrinsic to Rust. There are now 30k+ lines of Rust code [1] in the Linux kernel. C++ can't claim that.
I was also unaware that Rust has such significant penetration in the Linux kernel, and that's a place where I can see it really shining.
My first comment in this thread was something to the effect that Rust has tons of great stuff to offer, and that the memory safety argument is actually weaker than people think and probably not the only thing people should talk about.
The resulting gang-tackle is just one more data point that the community is still too small and evangelical for me to want to get involved past my proprietary Rust stuff.
I’m using it for a new project and honestly I’m using it more for the modern tooling and easy C interop than the safety features, but I’m a fan overall. Think it’s a really good language.
Can you expand on that? I'm currently researching something similar but lower level & lisp instead of Haskell. It would help to see some existing examples to figure out if it's worth it or not.
Basically we have a nice Haskell DSL for generating arbitrary C++, and we deal with lots of code you wouldn't want to write by hand (big nested switch statements and other kinds of state-machine logic, choices about loop unrolling, lots of template overloads, SIMD intrinsics that require immediate values, etc. etc.) so we write Haskell that generates C++ and feeds it to e.g. `clang`.
Some of this is directly in Haskell, and some of it is little compilers mostly done using Megaparsec. It's a really nice approach where it fits!
None of it is really all that magical btw, so you should take a whack at it yourself!
One thing that wouldn't have occurred to me that one of the Haskell geniuses I work with identified to use is the notion of a "free"/"operational" monad: https://apfelmus.nfshost.com/articles/operational-monad.html.
> None of it is really all that magical btw, so you should take a whack at it yourself!
Of course! It's always good to see how other people approach it though.
That's just as true of Rust if you use clone(), Rc<> and RefCell<>. You just have to familiarize with a few boilerplate patterns, and the best part is you're only trading off a modicum of performance while preserving safety. But Rust can work quite well as a language for exploratory programming.
Industry clearly prioritises speed of delivery above all else. Security, reliability and maintenance are future problems (that would be nice to have).
However, in order to gain the power to reason about our code and be able to prove the correctness of various properties (what a typechecker does), we have to program with more restrictive models. This has been argued many times before (e.g. Dijkstra's structured programming). Haskell and Rust are just two of many examples. My favourite is regular expressions, choose actual proper regexes and you have guaranteed O(n) execution, choose Perl/Python "Regex" and you have a potential security hole.
There was (and still isn’t) a competitor that can compete on the same level with Javascript in the browser.
<source>:21:35: warning: temporary whose address is used as value of local variable 'append34' will be destroyed at the end of the full-expression [-Wdangling]
auto append34 = make_appender({3, 4}); test.cpp:16:45: error: Using object that is a temporary. [danglingTemporaryLifetime]
assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB
^
test.cpp:3:12: note: Return lambda.
return [&](std::vector<int>&& items) {
^
test.cpp:2:50: note: Passed to reference.
auto make_appender(std::vector<int> const& suffix) {
^
test.cpp:4:36: note: Lambda captures variable by reference here.
return append(move(items), suffix);
^
test.cpp:15:35: note: Passed to 'make_appender'.
auto append34 = make_appender({3, 4});
^
test.cpp:15:35: note: Temporary created here.
auto append34 = make_appender({3, 4});
^
test.cpp:16:45: note: Using object that is a temporary.
assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB error: Syntax Error: AST broken, binary operator '!=' doesn't have two operands. [internalAstError]
explicit operator bool() const { return this->get() != pointer(); }
This is for a simple wrapper class that looks like this: template<class T>
class Foo : private Bar<T> {
public:
typedef value_type *pointer;
pointer get() const { return ...; }
explicit operator bool() const { return this->get() != pointer(); }
};
Another example: void foo(uintptr_t const (&input)[2]) {
if constexpr (sizeof(uintptr_t) == sizeof(int) && sizeof(long long) == 2 * sizeof(int)) {
long long value;
memcpy(&reinterpret_cast<uintptr_t *>(&value)[0], &input[0], sizeof(input[0]));
memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1]));
}
}
error: The address of local variable 'value' is accessed at non-zero index. [objectIndex]
memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1]));
^There isn't a right and wrong here: sometimes you want to opt in to the check for that, sometimes you want to opt out of it.
Sometimes I actually do want to fuck with addresses on the stack in weird, potentially architecture-dependent ways, it's rare but it happens.
I happen to think that Rust's linear/affine typing is by far the most usable low/zero-cost memory management model that anyone has demonstrated at scale and a real achievement in practical computer science, but it comes at a pretty serious cost in `Box`-this and `Arc`-that and `Rc`-other-thing and generally the borrow-checker being a PITA about some stuff we're used to doing.
Rust is very cool and I use it, but the "using C/C++ is fucking strangers without protection"-vibe got old years ago.
What matters is the lifetime, where the object lives is a rule of thumb for guessing lifetime that results from C++ trauma.
Getters that return references are code stink.
The bad C++ code is in the very first line of the "make_appender" definition: capturing the closure's environment by reference is nonsense: It is equivalent to returning a reference to an argument. It is not, then, a closure at all.
Using a correctly-defined make_appender would not, then, produce undefined behavior when you use it, with or without "move".
What the author has done here was to take a too-obviously wrong operation, returning a reference to an argument, but dress it up with syntax that will look less familiar to some readers, and pass it off as insightful.
But using a wrong function and getting wrong results is not surprising.
Piling on more uses after, that give more wrong results, does not reveal anything more.
When you need disingenuous arguments to make your point, it tells us more about your point than about the thing you are trying to make a point about. And, publishing anyway tells us more about you.
Returning that fake closure should evoke a compiler warning, if you turn on warnings.