
Rust vs. C++: Fine-grained Performance - beshrkayali
http://cantrip.org/rust-vs-c++.html
======
zvrba
Most insightful part for me was this one:

> Many variations [he's talking about Rust] that seemed like they ought to run
> the same speed or faster turned out slower, often much slower. By contrast,
> in C++ it was surprisingly difficult to discover a way to express the same
> operations differently and get a different run time.

This is an insidious pitfall that may earn Rust the "slow" label, akin to what
happened to Common Lisp.

~~~
pcwalton
> This is an insidious pitfall that may earn Rust the "slow" label, akin to
> what happened to Common Lisp.

This doesn't match my experience at all with Rust, for what it's worth. Not
having copy constructors makes up for any difference in that regard: in C++
it's way too easy to accidentally deep copy a vector and the entire object
graph underneath it--all it takes is "auto a = b" as opposed to "auto& a =
b"\--with very bad performance consequences. Rust, on the other hand, doesn't
let you deep copy unless you explicitly ask it to.

~~~
yongjik
I got so worried about accidentally deep-copying stuff that I ended up with an
ugly C++ hack:

    
    
        C(const C &) = delete;
        C &operator=(const C &) = delete;
        C(C &&) noexcept = default;
        C &operator=(C &&) noexcept = default;
    
        struct MakeCopy {
            const C &src;
            MakeCopy(const C &src) : src(src) { }
        };
    
        MakeCopy make_copy() const { return *this; }
        C(MakeCopy src) : field1(src.src.field1), (blah blah) { }
    

Then, when you really want to copy stuff, you have to say "a =
b.make_copy();".

This isn't really clean, because it's too easy to add a field to C and then
forget to add it to the "manual copy constructor", but so far I found it good
enough for me.

~~~
dikaiosune
In Rust, if you want a deep copy you (IIRC) implement the Clone trait, which
allows you to explicitly clone everything. Many collections in the standard
library already implement this, so you get it by default :).

Edit: I should point out that deep copies can only be explicit in Rust --
there's no implicit deep copy AFAIK.

~~~
steveklabnik
There is no implicit deep copy, Clone is usually how a deep copy is
implemented, but not all Clones are deep copies. Rc, for example, bumps a
reference count on Clone.

------
jasode
The author compares gcc vs rustc.[1]

I'm curious if the author considered comparing clang vs rustc since both use
the LLVM backend. (I'm guessing the more mature C++ code generation would
still win the benchmark but one could study the intermediate code emitted by
clang/rustc instead of the final machine code emitted by gcc.)

[1][https://github.com/ncm/nytm-spelling-
bee/blob/master/Makefil...](https://github.com/ncm/nytm-spelling-
bee/blob/master/Makefile)

~~~
jtxx000
On my Macbook Pro, the rust version seems consistently faster (20% or so) than
the c++ version compiled with clang.

~~~
jtxx000
As a followup, if I use __builtin_expect as recommended by the article, the
situation reverses, with c++ being significantly faster.

------
nkurz
_Curiously, most variations of the C++ version run only half as fast as they
should on Intel Haswell chips, probably because of branch prediction failures_

I haven't looked at the code yet (time for bed) but this note from Agner seems
like it might be relevant:

    
    
      3.8 Branch prediction in Intel Haswell, Broadwell and Skylake
    
      The branch predictor appears to have been redesigned in the 
      Haswell, but very little is known about its construction.
    
      The measured throughput for jumps and branches varies 
      between one branch per clock cycle and one branch per two 
      clock cycles for jumps and predicted taken branches. 
      Predicted not taken branches have an even higher throughput   
      of up to two branches per clock cycle.
    
      The high throughput for taken branches of one per clock was 
      observed for up to 128 branches with no more than one 
      branch per 16 bytes of code. If there is more than one 
      branch per 16 bytes of code then the throughput is reduced 
      to one jump per two clock cycles. If there are more than 
      128 branches in the critical part of the code, and if they 
      are spaced by at least 16 bytes, then apparently the first 
      128 branches have the high throughput and the remaining 
      have the low throughput.
    
      These observations may indicate that there are two branch 
      prediction methods: a fast method tied to the μop cache and 
      the instruction cache, and a slower method using a branch 
      target buffer.
    

[http://www.agner.org/optimize/microarchitecture.pdf](http://www.agner.org/optimize/microarchitecture.pdf)

At a glance, the symptoms seem like this might match. Separately, I think
there is still a limit of 3 branches per 16B before the branch prediction
starts to fail. It's rare to hit this in normal code, but something this
optimized might.

~~~
nly
I've recently experienced what I believe was likely branch predictor failure.
I rewrote essentially the same algorithm, with the same complexity, in a
completely new way, to reduce constant factors. The new code was beautiful,
and I was expecting perhaps 15-20% performance improvement. (40% of the
runtime in the old version was in one function, the new version should, and
did, call that function with less data to grind on average). Memory access
patterns should be identical. Grand.

So what happened with my standard benchmark on Haswell (GCC 5.3, -march=native
-O2)?

Old version, without PGO: ~8 minutes (baseline)

Old version, with PGO on: ~9 minutes (yes, PGO made it slower)

New version, without PGO: 11 minutes (60% slower)

New version, with PGO on: 7 minutes (~15% faster)

All of these benchmarks were replicated with a common set of functions tagged
noinline, and I inspected the assembly to see if there was a difference with
regard to loop unrolling. Nope, no unrolling. Afaict it appeared to be
entirely to do with the way GCC had laid out code with respect to branches.
The differences were minor in key areas and seemed like arbitrary compiler
choices.

I eventually narrowed this down with callgrinds branch-predictor simulator to
a few hot spots. I added a redundant if-statement to check for and short-
circuit a common case, even though it shouldn't have mattered, and a few
__builtin_expects, and suddenly I was getting the 15% speedup without PGO.

------
twic
I'm surprised by the small warts in the Rust code. In this line:

    
    
      let fname = &*env::args().nth(1).unwrap_or("/usr/share/dict/words".into());
    

Neither the &* not the .into() add much to the meaning for me as a reader. Why
are they necessary for the compiler?

Why is there an .unwrap() on writeln! ? To force a panic if the write failed?

~~~
steveklabnik
They used `into()` because it's the shortest. The more idiomatic options are
all of the other ones: String::from, .to_string(), and .to_owned() (Strings
implement a lot of generic interfaces.)

The &* converts a String to a &str. I would have done it in the match rather
than here, personally.

    
    
      > Why are they necessary for the compiler?
    

Rust does not heap allocate things automatically nor convert types
automatically. This is the side effect: you can see exactly how much
allocation is going on, and know exactly how stuff is being converted.

(We do have a very small and very specific set of conversions that will
happen, they don't here, though.)

    
    
      > Why is there an .unwrap() on writeln! ? To force a panic if the write failed? 
    

Yes. The compiler will warn you that you're ignoring the errors on the write,
and panicking is one way of handling it.

~~~
twic
So the string literal is a str, but it gets turned into a String with the
.into(), and then back into a str (well, &str) on the same line. That seems
like a lot of travelling to go a short distance.

Is this because args() ultimately gives you a String, and you need to get that
and the string literal out of a single expression?

Why does args() give you String rather than str? Couldn't command-line
arguments be static more-or-less constants?

Would there be any way to turn the output of args straight into a &str, so you
didn't need to .into() the literal?

Why are String and str separate types, rather than different uses of a single
type? Are there any other examples of pairs of types with the same
relationship? Arrays and slices? Any others? Is there something like this for
maps/dictionaries/hashes?

I realise i've asked a lot of basic questions about strings here, and that you
(and any other knowledgeable people reading this) are probably pretty tired of
explaining this over and over again. I would be equally grateful for a link to
some relevant documentation or whatever as for a detailed answer!

~~~
steveklabnik

      > Is this because args() ultimately gives you a String
    

Correct.

    
    
      > Why does args() give you String rather than str?
    

Because the command-line arguments to the program are dynamically created. To
be a &'static str, like a literal, they would have to be baked into the
binary. &str is a pointer, it doesn't own any backing store.

    
    
      > Would there be any way to turn the output of args straight into a &str, so you didn't need to .into() the literal?
    

Well, so that is what this code is doing. It's just also handling error cases:
What if the argument isn't passed in, as the primary one.

    
    
      > Why are String and str separate types, rather than different uses of a single type? 
    

Because String actually owns the data it's attached to, and is heap-allocated.
&str is a pointer; it can only point at some other string type, since it
doesn't store its own data.

    
    
      > Are there any other examples of pairs of types with the same relationship?
    

Arrays/Vectors and slices are a great example: String is wrapper around
Vec<u8>, and &str one around &[u8]. But any owned/pointer variant with some
sort of restriction on the type of data is going to be like this, it's a
pretty common pattern.

Don't forget other string types too: There's OSString, CString, and external
crates provide other types as well. Strings are not simple.

    
    
      > Is there something like this for maps/dictionaries/hashes?
    

Slices point to _contiguous_ spots in memory, so these data types don't really
have slices, because that property isn't true.

    
    
      > I would be equally grateful for a link to some relevant documentation or whatever as for a detailed answer!
    

Hehe, no worries. It's hard to say what _specifically_ would be the best docs,
but if you're interested in the _future_ docs about this, [http://rust-
lang.github.io/book/understanding-ownership.html](http://rust-
lang.github.io/book/understanding-ownership.html) is an explanation of Rust's
ownership system through String and &str that will become the next version in
the official docs. There's still some rough edges, as it's a draft, though!

------
Animats
I need to learn Rust again. I haven't used it in a year, and the language has
changed a lot. Everything now seems to require a closure or some ".into()"
idiom. Like this:

    
    
         let file: Box<Read> = match fname {
                "-" => Box::new(stdin.lock()),
                 _  => Box::new(fs::File::open(fname).unwrap_or_else(|err| {
                         writeln!(io::stderr(), "{}: \"{}\"", err, fname).unwrap();
                         process::exit(1);
                     }))
            };
    

Type Result has its very own set of control flow primitives.[1] So does type
Option.[2] Any type can have its very own flow control primitives. Are we
going to see

    
    
        date.if_weekday(|day| { ... }) 
    

and similar cruft for every type? I hope not.

Hopefully the setup of closures that probably won't be executed isnt't too
expensive. Do those require a heap allocation and release when not used, or is
this all on the stack? The run time variation for small changes indicates that
some constructs are more expensive than others, but it's hard to know which
ones are bad.

I understand the rationale behind the Rust approach to error handling, but
it's just painful to look at. After seeing the gyrations people go through in
Go and Rust to deal with the lack of exceptions, it looks like leaving
exceptions does not make a language simpler. From a compiler perspective, the
compiler can generally assume that exceptions are the rare case, and can
optimize accordingly. Rust has no idea which closure a function will call, if
any.

[1] [https://doc.rust-lang.org/std/result/enum.Result.html](https://doc.rust-
lang.org/std/result/enum.Result.html) [2] [https://doc.rust-
lang.org/std/option/enum.Option.html](https://doc.rust-
lang.org/std/option/enum.Option.html)

~~~
dbaupp
_> Hopefully the setup of closures that probably won't be executed isnt't too
expensive. Do those require a heap allocation and release when not used, or is
this all on the stack?_

Creating a closure with |...| { ... } is literally as expensive as creating a
tuple containing (references to) the variables it captures. That is, they're
on the stack by default like everything else in Rust, and there's no implicit
heap allocations. For more details, see, for instance,
[http://huonw.github.io/blog/2015/05/finding-closure-in-
rust/](http://huonw.github.io/blog/2015/05/finding-closure-in-rust/) .

 _> Rust has no idea which closure a function will call, if any._

It does. As my link above discusses, each closure has a unique type, allowing
monomorphisation to kick in and hence the compiler can easily optimise and
inline calls to closures (as long as the author of the closure-taking function
doesn't opt-in to only allowing virtual dispatch).

------
ufo

        Incidentally, I don’t know why I can write  
            let (mut word, mut len, mut ones) = (0u32, 0, 0);
        but not
            (word, len, ones) = (0, 0, 0);
    

Marking variable declarations with _let_ is a good thing and makes it clear if
something is an assignment or a variable declaration. It also leads to cleaner
code when you use nested functions (I always hated the "nonlocal" keyword in
Python)

~~~
losvedir
I think the author was indicating that while you can use that syntax to
declare and initialize variables, you can't use it later to assign new values
to them. I don't think they had any problems with the concept of `let`.

And it does strike me as a little weird as well to only be able to take
advantage of the pattern match destructuring on initialization, but I'm sure
there's a good technical reason.

~~~
deng
I read it the same way. There's an open issue for it:

[https://github.com/rust-lang/rfcs/issues/372](https://github.com/rust-
lang/rfcs/issues/372)

I think one of the main reasons this isn't done is because of the grammar
issue. I dimly remember that one of the goals of Rust was to be parseable with
a LL(1) grammar.

~~~
steveklabnik
There are grammar issues, yes. We unfortunately have one tiny case that's
context-sensitive though, so we didn't quite get there :/ that vast majority
of the grammar is very straightforward though. There's a lot of benefit to it.

------
Ace17
If you want to compare the verbosity of both languages, why not use 'using
namespace std' ?

Why write:

> std::vector<unsigned> counts; counts.resize(sevens.size());

When you can write:

> auto counts = vector<unsigned>(sevens.size());

Doesn't seem like a fair comparison to me.

~~~
svalorzen
I don't agree on the general usage of "using namespace std", so that part does
not bother me.

For an even shorter init:

> std::vector<unsigned> counts(sevens.size());

~~~
rosshemsley
Bringing namespaces into scope is perfectly idiomatic C++, just don't do it in
your headers.

~~~
autoreleasepool
IIRC, importing an entire namespace into scope is a feature that was added
exclusively for backwards compatibility and is not idiomatic modern C++.

What _is_ idiomatic is explicitly importing individual symbols into a name
peace.

    
    
        #include <iostream>
        #include <vector>
        #include <string>
        
        using std::cout;
        using std::vector;
        using std::string;
    
        int main() { 
        
          vector<string> v {"explicit ", "is better ", "than implicit\n" };
    
          for (auto p: v) cout << p;
          
          return 0;
          
        }
    

I'm on mobile so please forgive errors.

~~~
2bitencryption
Wow, I never knew about that. I always hated having std:: everywhere, just so
I could have "correct" C++. But I also hated referencing to std implicitly.
This is the perfect tradeoff: explicitly state intentions in the beginning,
then implicitly reference them as you go. (works until you use eighty five
billion lines just to import your symbols)

------
vkjv
I think one thing of note here is that the author did _not_ want to make the
program parallel. One of the benefits touted by Rust is "fearless
concurrency." If this feature was introduced this is where # lines or
performance may have diverged.

~~~
sitkack
Parallel is of course very important. But if serial speed isn't in the same
range, go parallel will be the same perf crutch as the Python folks who say
drop to native. Rust programs shouldn't be parallel to beat a single threaded
C++ program.

~~~
toolz
>Rust programs shouldn't be parallel to beat a single threaded C++ program.

I don't write code in either language, but as an outsider my impression is
that rust was created to make programming for parallel execution, easier.

If that's the case, why are we comparing a use case c++ is optimized for
against a use case rust isn't optimized for?

~~~
steveklabnik
We do want Rust to be excellent at parallel execution, but that does not mean
that we don't pay attention to single-threaded performance either. The way
that we make paralell/concurrent code better has no negative impact on single-
threaded performance. In fact, sometimes you can use more efficient data
structures when you know that you're not using multiple threads, like Arc<T>
and Rc<T> for example.

~~~
sitkack
Exactly. Most C++ codebases lean too heavily on `shared_ptr` because they
don't/can't know when something will cross a thread boundary.

------
shin_lao
To my knowledge std::ifstream is pretty slow, I think the author should load
the whole file in memory with a cache friendly layout and try the benchmark
again.

------
hellofunk
>but I’m amazed that Intel released Haswell that way. I don’t know yet if it
Intel fixed it in Broadwell or Skylake.

I would love to know more about this; how curious indeed.

------
threeseed
This is really fantastic to get an understanding of how Rust works in a real
world environment. As a non-systems developer I am finding the Rust language
to be a little cryptic though in particular the many new syntax elements that
don't exist in other languages.

I would be curious to know if there are plans like Swift to simplify the
language syntax in the future or if it is just something to get used to.

~~~
steveklabnik
It depends on what you mean by "simplify". We've just accepted an RFC for ?,
for example, which should make certain code more concise, but it is an
additional feature, so is that more simple?

~~~
quickben
General notes: \- Semicolons inconsistent usage makes Rust harder to process.
Is that semicolon extra? Is it not? Will that be a compilation error? Saving
single keystrokes will be paid in cognitive load. Programming is already a
demanding task, this isn't helping. \- Chaining multiple statements on a
single line is bad for various reasons.

Dependencies / headers / modules: \- C++ is better precisely because it's more
grained on the initialization side. The paragraph starting with 'Rust wins,
here', is just a general turnoff to read the rest of the article. When I read
the title 'Rust vs. C++: Fine-grained performance' I was honestly expecting
either a table with performance times across various algorithms, or a savvy
optimization guide. I wasn't expecting a LoC duke-it-out.

Input file processing \- Both are conceptually boilerplate code. There is more
for Rust apparently. \- There is an interesting &⊛env:: in there that makes
the language looks like it needs a bit more work. Three operators just to do
something with the first object? \- Are they intentionally trying to copy C++
with the :: operator but removing visually arbitrary semicolons? Just replace
the :: with something else.

Data structure and input setup \- Remove the 'let' from the language, and it
does look cleaner than the C++.

Input state machine \- Why isn't "(0..7).rev()" "(7..0)" \- On the bottom
part, lamba-like usage will makes in less comprehensible (and probably
undebuggable the way it's written)

General lack of performance numbers \- _" I found that iterating over an array
with (e.g.) “array.iter()” was much faster than with “&array”, although it
should be the same..." "Curiously, changing scores to an array of 16-bit
values slowed down (earlier versions of) the C++ program by quite a large
amount – almost 10% in some tests – as the compiler yields to temptation and
forces scores into an XMM register. The Rust program was also affected, but
less so."_

Edit: Unicode

~~~
steveklabnik
I didn't write this blog post, though I did help the author optimize their
Rust code when they dropped by #rust, so I'll respond to the generic Rust
stuff rather than those details.

    
    
      > Semicolons inconsistent usage
    

You've asserted this, but not demonstrated it: semicolon usage is consistent
in Rust. Rust is not C++, so it doesn't necessarily follow the same rules, but
it is consistent, even though it may be different.

    
    
      > C++ is better precisely because it's more grained on the initialization side.
    

Can you elaborate on this? I'm not exactly sure what you're saying.

    
    
      > There is an interesting &⊛env:: in there that makes the language
      > looks like it needs a bit more work. 
    

See above: this kind of thing is about how Rust views coercion and heap
allocation: explicit, not implicit.

    
    
      >  Just replace the :: with something else.
    

We tried that. We still ended up with :: as a scope operator.

    
    
      > Remove the 'let' from the language, and it does look cleaner than the C++.
    

`let` has a few advantages, in that it allows you to take advantage of
patterns, and makes initialization explicit.

    
    
      > Why isn't "(0..7).rev()" "(7..0)"
    

Because ranges always iterate forward, from start to end. rev() reverses the
direction.

    
    
      > On the bottom part, lamba-like usage
    

I'm not sure what you're referring to here.

~~~
quickben
I get the feeling that Rust design is concerned more about write-ability of
the code than readability. Code has to be maintained, read by random people,
comprehended. It will grow up in hundreds of megs on large projects, and it
will take time to compile.

With the above in mind:

> You've asserted this, but not demonstrated it: semicolon usage is consistent
> in Rust. Rust is not C++, so it doesn't necessarily follow the same rules,
> but it is consistent, even though it may be different.

Writing code is consistent and unaffected, code comprehension (debugging,
integration efforts, etc) by third parties will be hampered.

>Can you elaborate on this? I'm not exactly sure what you're saying.

Not importing the entire library will result in faster compilation. Unless
rust has a concept similar to precompiled headers, etc. I apologize at this
point. I'm not sure what Rust does in that respect, but my initial thoughts
were that as the project grows, the parser will slow on includes. The internal
symbol resolver will slow on lookups because too many things are included.

All of the above is only if you are trying to match what C++ does for large
teams. I guess it's up to the developers of the language what direction they
want to take it in. More agile or more enterprise oriented.

(From personal experience: I was on a team looong time ago, where codebase
took 12 hours to compile on a powerful cluster. Any gains, even by few hours,
changed schedules drastically)

>See above: this kind of thing is about how Rust views coercion and heap
allocation: explicit, not implicit.

I understand your viewpoint, however three operators on a single object still
seems conceptually excessive to me.

>Because ranges always iterate forward, from start to end. rev() reverses the
direction.

Since both are supported, maybe the parser can be made smarter?

> I'm not sure what you're referring to here.

Well this piece of code:

    
    
      let scores = words.iter()
                .filter(|&word| word & !seven == 0)
                .fold([counts[count];7], |mut scores, &word| {
                    for place in 0..7
                         { scores[place] += (word >> bits[place]) & 1; }
                    scores
                });
    

Is not easily read or debugged ( on which line do you set the breakpoint,
etc).

Outside of minor things like this, I like what Rust is trying to do. I think
it's a great effort and I do look forward to where it will lead.

~~~
dbaupp
_> Not importing the entire library will result in faster compilation. Unless
rust has a concept similar to precompiled headers, etc. I apologize at this
point. I'm not sure what Rust does in that respect, but my initial thoughts
were that as the project grows, the parser will slow on includes. The internal
symbol resolver will slow on lookups because too many things are included._

The compilation time problems associated with headers are problems with
headers: avoiding headers makes it easy to avoid the problems. The crate
system of Rust is somewhat similar to precompiled headers, and C++ is in fact
in the process of putting something similar (modules) into the standard.

Interestingly, this makes the Rust compiler easier to work with: the basic
things of lexing, parsing, name resolution etc. are not at all bottlenecks
(any particular file is only parsed once, there's not megabytes and megabytes
of headers to parse for every invocation). This means these parts don't have
to be micro-optimised, and hence don't get hit by an increase in complexity
due to those optimisations.

Concerns about internal symbol resolution aren't a problem: hashmaps give
(expected) O(1) access no matter how many elements they hold, and even using a
tree has O(log n) access (i.e. going from 1_000 symbols to 1_000_000 only
doubles the time for a look-up, and the next doubling happens at
1_000_000_000_000 symbols).

 _> Since both are supported, maybe the parser can be made smarter?_

Not sure what this has got to do with the parser (7..0 _parses_ fine, it just
creates an empty iterator), but implicitly reversing ranges are on of the most
annoying features of R: it makes it annoyingly hard to do things like `x..y -
3`.

 _> Well this piece of code:_

Two things: the author is effectively golfing here (they say they want the
code to fit on a single page), and, the Rust version can be written just like
the C++ version, with a loop:

    
    
      let scores = [counts[count]; 7];
      for &word in words {
          if word & !seven == 0 {
              for place in 0..7 {
                  scores[place] += (word >> bits[places]) & 1;
              }
          }
      }

~~~
steveklabnik
Can LLVM lift the bounds check out here?

~~~
dbaupp
Yeah, it should be able to handle that easily, since both bits and scores have
static length 7. Note, however, that the loop version does no more or less []
indexing than the iterator/fold version.

------
zerr
It seems Rust sacrifice too much for the sake of safety. If Swift will evolve
to truly cross-platform language, I think it is in the golden middle of safety
vs usability trade-off (without requiring non-deterministic GC like D).

~~~
autoreleasepool
I think after about Swift 4.0, if you're writing a native application in
[systems language] and you're using Arc/Rc/shared_ptr all over the place you
need to seriously take a step back and ask yourself why you're not using
Swift.

If Swift had structural references (for all intents and purposes in Swift, all
structs are value types and all classes are automatically reference counted
pointers) and move semantics Swift would absolutely kill it.

It's an exciting time to be a PL nerd.

~~~
kibwen
I'm a Rust fan, but I'll still be excited if Swift manages to add some sort of
borrow checker to the language in a future revision (C++, D, and Nim have all
also at least alluded towards such a direction, but only C++ has made any
progress so far). From what I've seen though, I'm still not sure that Chris
Lattner has given serious thought to such an addition. I'm waiting for a
concrete proposal before getting my hopes up, because Rust had to make put
significant effort into tailoring the base language to borrow checking and
implementing such an analysis after the fact runs the risk of serious
divergence (or worse, breakage) in the ecosystem.

(There's also the fact that, as of the hypothetical Swift 4k, none of the
library ecosystem will be leveraging the borrow checker and so Rust will still
have a substantial head start for people who want thoroughly static
ownership.)

Other than that I'm a bit dismayed that Swift just punts concurrency to GCD,
given how many other new languages make concurrency such a major focus
(especially Rust).

------
anfroid555
Rust = c syntax with Erlang conventions

~~~
pdpi
You would've hit closer to home if you'd said "ML" or "Haskell" instead of
Erlang, though you'd still be shooting wide.

Rust's borrow checker is its single most distinctive feature, and that is
inspired by the work done in the Cyclone language.

------
cppishellacool
Do you want C++ compiler to also prevent data races? Then use const functions
by default, seriously.

[http://youtu.be/Y1KOuFYtTF4](http://youtu.be/Y1KOuFYtTF4) (starts at 29:00)

C++ template meta programming language is such a powerful and expressive tool
for writing libraries and safe abstraction. Imagine if all the effort of 8
years of Rust team went into a modern C++ safety library.

Such a library would have been more useful contribution and more people would
have benefited.

~~~
steveklabnik
Using const does not prevent data races.

We made Rust because you can't actually retrofit it's guarantees on C++
without breaking backwards compatibility. The CPP Core Guidelines are an
example of this: Herb said that data race prevention is a non-goal, and in
general, how it interacts with concurrency is not yet understood.

~~~
cppishellacool
Did you watch the video?

Curious why you think using const _functions_ doesn't prevent data races after
watching

at 29:00 he says the C++ standard guarantees const member functions are data
race free.

~~~
steveklabnik
I mean, data races need mutability, so something that's const can't have a
race, sure.

What I meant was something like "const itself is not a panacea against data
races generally." It is a useful tool.

~~~
cppishellacool
Don't get me wrong I respect Rust core team as theorists and think rust is an
important research project.

But in C++ if you have a const member function F() then F() can only change
member variables that use the "mutable" key word.

If you default to all methods are const you have much much safer C++. As safe
as rust? not probably but combined with liftimes proposal and static analysis
rules we have a language good engineers can sufficiently work with.

    
    
        struct Foo 
        {
             mutable auto a = 4;
             auto increment() const -> void 
             {
                  this->++a;
             }
        };
    

If we train engineers to use const member functions, then the programmer is
forced to think about state and mutation.

As C++98 PTSD begins to fade and C++17 becomes the cultural mental image of
the language, the argument for switching to rust will become less and less
compelling.

Eventually, the only "advantage" will be ML influenced syntax.

The reason is there are only three languages that can easily call into C++
code. Objective-C (swift by proxy), D, and of course C++.

Why rust didn't prioritize compatibility with C++ like C++ did with C boggles
my mind. Why do you think C++ became so popular? Near perfect C compatibility
(until recently) is literally the only reason. Instead of focusing on
pragmatisim Rust focused on language purity.

At what cost?

~~~
pcwalton
> But in C++ if you have a const member function F() then F() can only change
> member variables that use the "mutable" key word.

This is not nearly enough. See the examples I gave in my other post.

Besides, all "const" means is "I won't mutate 'this'". It doesn't mean someone
_else_ who has a non-const reference to your object won't mutate your object.
That makes it largely useless for reasoning at a local level.

> If we train engineers to use const member functions, then the programmer is
> forced to think about state and mutation.

The programmer may think about it, but the compiler doesn't enforce it at all.

> As C++98 PTSD begins to fade and C++17 becomes the cultural mental image of
> the language, the argument for switching to rust will become less and less
> compelling.

Only if you don't understand how Rust works.

> Why rust didn't prioritize compatibility with C++ like C++ did with C
> boggles my mind.

Because C++ compatibility is impossible without being a derivative of C++, and
C++ is hopeless in the safety department. I believe it is impossible to make
C++ memory safe without making it not C++ anymore.

> Instead of focusing on pragmatisim Rust focused on language purity.

No, Rust focused on what is _possible_.

~~~
autoreleasepool
> Because C++ compatibility is impossible without being a derivative of C++,
> and C++ is hopeless in the safety department. I believe it is impossible to
> make C++ memory safe without making it not C++ anymore.

Do you consider D to be a derivative of C++? I don't. They have a near perfect
C++ interface. D can even catch C++ exceptions. It's pretty sweet. You can
hear about the black magic Andrei used to get that to work here:

[http://cppcast.com/2015/10/andrei-
alexandrescu/](http://cppcast.com/2015/10/andrei-alexandrescu/)

Side note: I'm actually thinking the Rust and C++ comparison misses the mark.
First of all, C++ cannot be obsoleted by rust, it's economically impossible.
Secondly, I don't even think they compete in the same domain space. Rust is
actually lower level than C++, in that it does lower level stuff in a more
natural way. It's C, that Rust makes obsolete; which is something C++ totally
failed to do (because of it's RTTI, exceptions, and other runtime features --
all things you and the rest of the Rust team intelligently scrapped). After
trying embedded Rust, I'm completely done using C (and the C parts of C++).

IMO the advantages C++ still has over Rust are:

\- libraries, libraries, libraries

    
    
        - C++ has so many great libraries
    
        - *writing* C++ libraries with SFINAE sugar is the bees knees.
    

\- much better support and tooling, but this is for sure a temporary
advantage.

\- template meta-programming feels like a different language, it's dynamic,
functional, and the difference is refreshing. I like the mental switch you
have to do to go from writing a C++ template to writing a C++ model.

\- as much as header files suck, they let you skim an API much more easily
than Rust or Swift.

\- performant, memory safe data structures:

For example, it's trivial to write a low overhead pointer that can't dangle,
throws an exception on nullptr deference, and that safely points to an object
it doesn't own (even if that object is its owner). With this and a unique_ptr
composed with similar safety mechanisms you now have the primitives needed to
write any data structures or algorithm so that it is protected from undefined
and unsafe behavior, yet remains performant. This solution has significantly
less overhead than using reference counted pointers like shared_ptr
derivatives or Rc. The performance tradeoff over using raw pointers is
minuscule and totally dwarfed by the gain in safety guarantees. From what I've
observed, you can't do this in Rust, if you try to write a doubly linked list
you either have to use C-pointers that can dangle inside unsafe blocks, or eat
the Rc overhead.

EDIT: How could I forget to mention the glory that is constexpr?

Advantages of Rust over C++ (again, IMO):

\- the correct defaults (aka opposite of C's insanity)

\- best C++11 features built in and defaulted (move semantics, unique_ptr is
Box<T>, Rc is shared_ptr etc..)

\- Bad ass embedded capabilities. You don't nearly feel as crippled like you
do in C++ without the STL.

\- the safety guarantees are real, and they're awesome

\- move semantics into closures looks so goddamn elegant. I love it

\- inline assembly doesn't feel like a hack

\- modules and cargo rock

\- secretly and surprisingly similar to ruby in a lot of places :)

\- it wants you to think functionally, and unlike Swift, it means it. This is
a good thing.

~~~
dikaiosune
> as much as header files suck, they let you skim an API much more easily than
> Rust or Swift

Just to reply to this individual point, rustdoc is _fantastic_. Personally I
much prefer having an HTML-rendered "header file" for a crate's API. Even if
they aren't hosted, it's trivial to generate one yourself using `cargo doc`.
To be maximally useful, the author needs to have made doc comments, but at
least there's a standard format for that, and you might not have anything like
that in a header file anyways.

~~~
heinrich5991
You can try crates.fyi for hosted docs, e.g.

[https://crates.fyi/crates/itertools/0.4.7/](https://crates.fyi/crates/itertools/0.4.7/)

