15 ms·
Safety: A comparaison between Rust, C++ and Go
- benreesman 4y agoRust has a lot of great qualities that C++ lacks, but comparing `rustc` to `gcc` or `clang` on move-semantics checking is just kind of silly these days. `rustc` has `clang-tidy` built in. `clang-tidy` is not letting you mutate or even access that moved-from "suffix" object without throwing an error. It's annoying that you need `clang-tidy` and ASAN and shit to get comparable runtime safety even in greenfield C++, but that's not why to prefer Rust. Prefer Rust because of a better traits system and pattern matching and syntax for `Maybe/Either` and a consistent build/library story and a number of other things.
- blakehawkins 4y agoAnd way, way better compiler errors thanks to a sane generics system
- benreesman 4y agoAnd much better compiler errors. `rustc` about leads the pack on error messages of all the languages I use regularly.
- bbojan 4y agoI think you meant thanks to a sane(r) macro system? Both Rust and C++ use monomorphisation for generics, I believe shitty compiler errors are due to C++'s templating.
- throwaway17_17 4y agoI’m what way can you have ‘generics’ in C++ that are not based on templating? I am almost certain that any implementation of anything ‘generic’ templates are inherently involved. Maybe I’m wrong about what you mean by generics though.
- estebank 4y agoThere are concepts now which are close enough to Rust's traits.
- estebank 4y agoYour point is a fair one, but defaults matter. There's little reason for clang-tidy not to be part of the default clang invocation by now, other than an aversion to producing new output for existing projects (that you could argue are already "broken"). Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then.
- dataflow 4y ago> Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then. Confused... so you're suggesting Rust's checks are somehow free of false positives? Doesn't the halting problem get in the way? One Rust-specific example: https://www.reddit.com/r/rust/comments/nr7a33/is_the_borrow_checker_wrong_here/ https://www.reddit.com/r/rust/comments/nr7a33/is_the_borrow_...
- estebank 4y agoThere's a difference between code that used to compile no longer compiling because of an incorrect lint, and code that was never accepted. Rust is restrictive and gets less so over time. C and C++ need to become more restrictive over time, but that's a more traumatic direction.
- dataflow 4y agoWhat you're actually arguing seems to be "why I like Rust more than C++", not arguing why "clang-tidy has false positives, and thus the comparison isn't apples to apples then". Clearly the positives can be just as false in Rust as in C++. Your actual objection is that anyone arguing that any feature of Clang can measure up to the corresponding feature of Rust at present is automatically disqualified from making that argument because... Clang's past "taints" its present? Like an original sin of sorts, but in programming? ("Apple" forbidden against comparison?)
- estebank 4y agoIt's not about an original sin, it's about the practicalities of changing the default behaviour without pissing off your users.
- deleted 4y ago[deleted]
- blub 4y agoObjectively speaking, Rust does not "have a lot of great qualities that C++ lacks". Any new feature or improvement has its pluses, but also its minuses. * Rust has traits, but does not support OOP. Architectures where OOP is particularly effective are proving to be a significant challenge for Rust - GUIs are the obvious one, but also game development Rust projects have to invent new approaches. * Option/Result make the code flow obvious, but having to return them in nearly all function calls is tedious. Rust has had several attempts at alleviating this problem, but still hasn't matched the convenience of exceptions. * match is IMO syntactic sugar and its benefits for correctness are being oversold. The fact that one is basically obliged to use it leads to code that's sometimes too deep. Exceptions would cut through this error-handling noise, if they were available. * The build story is convenient, but this had the unintended effect of encouraging dependency explosion. Adding typical crates results in dozens of transitive dependencies being included in a project. The notable improvement that Rust brings to the table is machine-verified memory safety with C++-like performance, but that of course comes at the cost of having to adapt code to what the borrow checker understands. If one needs the feature, then the cost is worth paying, if not, Rust is more of a personal choice than an inevitable conclusion. And finally, perhaps Rust's biggest sin is that it's big, it's complex and there's no end in sight to the complexity spiral, just like for C++. I can only imagine the chagrin of the Rust community as they see themselves competing with Go for many projects where performance is not absolutely critical and often being second choice exactly because of this complexity.
- ncmncm 4y agoMore to the point, Rust lacks key features I need to capture essential semantics into libraries. So, the libraries I could write in Rust would be less powerful than libraries I can write in C++. Among common uses for these more powerful features is to make misuse of the library into a compile-time error. Coding the library in Rust, if possible at all, would mean failing to prevent these usage errors. The point here is that C++ puts more power in the hands of a library writer, and both the responsibility and capability to enforce safety in uses of the library. Rust jealously reserves maintaining safety to the compiler alone.
- 4y ago
- RobertWHurst 4y agoTo say the rust has a built in linter is wrong. A rust program that does not build because of a memory ownership error on the part of the programmer isn't rejected by the compiler due to a detected pattern, it is rejected because the program is "unsolvable" and cannot be built. I think a lot of people miss how integral the memory semantics Ruct enforces are to how it parses and compiles programs. If these things were simply lints it could be reasoned that a program could be built without following these semantics, that If you could simply go into the compilers source you could just turn them off/remove them. You can't. The way rust's compiler tracks memory is fundamental to how it compiles the binary. It is not simply pattern matching code or ast. Rust's compiler is actually tracking the lifecycle of every bit of memory allocated so it knows when to free it, and it does this at compile time without running the program. These memory semantics errors exist because they are integral. Turning them off would simply result in a broken compiler, or a program with no freeing of memory because the compile time reference counting rust implements becomes impossible.
- jiehong 4y agoJava falls a bit in between Go and Rust on that example: Closures only allow variables that are effectively final, aka you can’t reassign them (the compiler will stop you). But you could pass an object and update its internal state.
- qsort 4y ago"final" in Java is kind of useless, really. A const mechanism like C++ has would go a long way and would be a perfect fit for the OO nature of the language.
- eganjs 4y agoFinal in Java means the value of a variable, property or parameter will not change after its initial assignment. Values in Java can be either a reference to an object or a primitive such as an integer, double or bool. It's definitely far from useless as it asserts that the reference or value you capture in a closure is not an old version that has been replaced, this approach is a consequence of Java disallowing arbitrary pointers. IMO Java's biggest mistake here is mutability by default, which Kotlin has learned from. If you understand why it is this way it makes a lot of sense and tbh I think it promotes better code. That said I would like to see more immutability in Java and with things like Record classes you can see Java is moving in the right direction.
- qsort 4y agoI agree with that and I use final as much as possible. E.g. instance variables that don't need to change, I almost religiously declare them as "final" and initialize them in the constructor. What I mean is that what Java really "should" have is const like C++. A C++ function with a prototype of: int doSomething(std::vector<int> const &x) tells me much more than the equivalent Java: int doSomething(final List<Integer> x) Also C++ member functions can declare themselves as not modifying their "this" instance. E.g. there's no way to write this code in Java: int X::doSomething(std::vector<int> const &x) const { ... } which is an extremely powerful, compile-time checkable description of what we are doing. It's not that final is useless (probably wrong choice of words there), what it does is okay and it's correct to use it as much as possible, but it's a far cry from the static guarantees afforded by const-correct code. I also agree that the correct approach is immutability by default, but that ship has sailed, and it's also an orthogonal concern to what I'm saying here.
- wruza 4y agocomparaison Probableur a typeaux?
- deleted 4y ago[deleted]
- rob74 4y agoWas this posted by a French speaker? Because in English it's "comparison", not "comparaison"...
- ncmncm 4y agoDisingenuous. 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.
- deleted 4y ago[deleted]
- benreesman 4y agoI was trying to be a little more diplomatic in my sibling reply because I've locked horns with the Rust community before and not enjoyed it, but you're not wrong.
- roca 4y agoA function returning a value that depends on the lifetime of the function's parameter is not crazy at all. Every class getter method that returns a reference to a member of the class does this.
- benreesman 4y agoSure. But returning the address of a stack-allocated object is (usually) broken. 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.
- alrlroipsp 4y agoScreenshot absolutely make me •¶Žš‰»‚¯
- jayp1418 4y agoWhat about comparing to Ada Programming Language?
- vitus 4y agoI feel like a better comparison would be to use std::span (C++20) to mimic Rust's slice. Otherwise you might be tricked into thinking that adding // hey don't pass in a temporary auto make_appender(std::vector<int>&& suffix) = delete; (which turns the provided code into a compiler error under both g++ and clang++) is adequate to prevent the immediate class of issues (namely, C++ allows const Foo& and Foo&& to bind to temporaries). Meanwhile, it's really really easy to make std::span dangle: #include <cassert> #include <span> #include <utility> #include <vector> std::vector<int> append(std::vector<int>&& items, std::span<int> suffix) { items.insert(items.end(), suffix.begin(), suffix.end()); return items; } auto make_appender(std::span<int> suffix) { return [=](std::vector<int>&& items) { return append(std::move(items), suffix); }; } auto make_appender34() { std::vector<int> vec = {3, 4}; return make_appender(vec); } int main() { auto append34 = make_appender34(); assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); }
- ncmncm 4y agoThat would just be another tendentious example. Nobody would write a make_appender that takes a span argument, because it makes no sense. The point we should take away is that is actually hard to invent plausible examples of the failure that we are being told Rust would prevent.
- vitus 4y ago> Nobody would write a make_appender that takes a span argument, because it makes no sense. I don't agree with that. If you can guarantee that the data pointed to by the span will outlive your appender, then it's safe. And if you don't actually want to transfer ownership or incur the overhead of a copy, and you don't care if your input is a vector or an array, then it's the correct abstraction. Replace std::span with std::weak_ptr (or a raw pointer), and replace the closure with a class (e.g. a tree where each node has a weak pointer to its parent), and tell me again that nobody would ever write that code. It's fundamentally the same concept: if your ownership model isn't ironclad, or if any of your assumptions are ever violated, then you can run into use-after-free.
- kcartlidge 4y agoTotally not the point of the article, and totally subjective I know, but to me the thing that jumps out is how much more readable the Go code is than the others.
- deeptote 4y agoThis is the main reason that I use Go as my main language, and why many orgs are starting to adopt it: it's easy to read and jump into. I would argue, however, that it's very easy to create antipatterns and just general spaghetti code with Go. A language that's easy to be productive with != one that's also easy to maintain. Design and philosophy becomes very important with large codebases in the language. source: consultant, seen some truly heinous Go monoliths.
- bsaul 4y agoThere are definitely some ways to do bad designs in go, but i have the feeling it will be more immediately apparent what's wrong (or at least what part of the system needs rework). The reason being that there are no ways to obfsucate an awful design by wrapping it on mountains of generics programming and language sugar, making the whole thing a lot worse. It's only my gut feeling, but does that match your experience ?
- arriu 4y agoI agree. However, my gut reaction was that the style of the go code was written differently than what I’d expect if asked to work off the rust version.
- deleted 4y ago[deleted]
- agent281 4y agoDefinitely subjective. I don't find some parts very readable. E.g., this line took my a minute to parse: append34 := func() func([]int) []int { If I was going to rank the readability I would say: 1. Rust function bodies 2. Go code 3. Rust function signatures 4. C++ code Which you could argue is me shifting the boundaries a bit, but sufficiently statically typed languages seem to develop two (or more) sublanguages. Global complexity definitely pushes Rust down peg.
- DmitryOlshansky 4y agoThe fact that the GC “owns” all of the memory happens to help a lot with lock-free stuff.
- jeffbee 4y agoThere's a solid argument in here but it feels like there must be a better example. Can we think of a function that does something worth doing, in a way that programmers of all these languages would actually use, and that sets a subtle trap for C++ programmers? When I read this article all I see is a useless function that contains a completely obvious trap which, yes, Rust prevents, but also just thinking at all would have prevented. Another small thing: the C++ in this article looks weird to C++ programmers because it qualified vector with std, but does not qualify move.
- TinkersW 4y agoThis would be a better article if the UB in the C++ example wasn't blindingly obvious-- no C++ programmer worth a damn would ever write this.
- jrajav 4y agoWhy is this such a common defense for the peculiarities of C++? I see it pop up at least a couple of times, anytime C++ is criticized. I don't have anything against taking pride in one's skill and craftsmanship, but excusing a tool's failings purely on the basis that one needs more skill to wield it and avoid those failings? I want to have that same level of skill and have my tool multiply my skill's output to the max, not have my skill wasted coaxing my tool to perform correctly. If the implication is that the tool requiring more skill gives commensurate benefits, fair enough, but not if it's just hand-waving away obvious downsides.
- TinkersW 4y agoBecause the code in question isn't something a C++ dev would write, instead it looks like something someone who doesn't use the language much if at all, decided to use for this pretty silly comparison. It would be one thing if this required skill, but this example is downright silly. Some other people here have posted more reasonable version, that might actually occur in the real world(like the example with std::span)
- pfultz2 4y agoYea and C++ static analysis tools already warn for the case, so even for new C++ programmers where it might not be entirely obvious, its still easy to catch the error.
- sagarm 4y agoYeah, agreed that a competent C++ dev would not write the code in the example. The charitable interpretation though is that errors of the same type can crop in real codebases, the example is just simplified for the purposes of discussion.
- 4y ago
- phendrenad2 4y agoCan we throw Python in too? Python has more safety than any of these languages, yet it's always left out of these discussions.
- aliqot 4y agoIs this satire?
- pdimitar 4y agoYou're joking, right? A dynamically-typed language has more safety?...
- phendrenad2 4y agoWhat is Python lacking that would make it as safe as Rust?
- niekb 4y agoThe author could compile c++ with the sanitizers, i.e. -fsanitize=address,undefined and make a make_appender function that leverages perfect forwarding...: template<typename S> auto make_appender(S&& suffix) { return [perf_fwd_suffix = std::tuple{std::forward<S>(suffix)}](std::vector<int>&& items) { return append(std::move(items), std::get<0>(perf_fwd_suffix)); }; } see: https://godbolt.org/z/M9P4MK4a8 https://godbolt.org/z/M9P4MK4a8