
“I just started writing some Rust” - whoisnnamdi
https://twitter.com/id_aa_carmack/status/1089286703817412608
======
twic
> "Yup, all the things you mentioned are also available in C++, and you've
> been ignoring and criticizing them for years"

Even that's not true.

One of the practically valuable things about Rust is that it has a decent and
standardised package manager and build tool. C++ does not, and is nowhere
close to having that.

Another is that the Rust libraries use iterators as a straightforward,
composable, and efficient way to write operations on streams of values. The
C++ standard library doesn't have anything like that.

Rust has a really powerful macro system. C++ doesn't. It has template
metaprogramming, which can do most of what macros can, but it's agony to use.
C++ is getting some really powerful compile-time programming facilities, but
those aren't in a standard yet.

There are a lot of things in Rust that are in C++. There are things in C++
that are not in Rust (some of them even useful!). But there are things in Rust
that aren't in C++, and when C++ loyalists say there aren't, they just look
stupid.

~~~
tomnj
I love C++ (though it often frustrates me), and I think Rust is doing some
awesome things (though I don’t work in it). A few clarifications:

Your point about iterators and composability: you’re right, the STL suffers a
bit here, but Eric Neibler’s ranges library (which is partially approved for
C++20) addresses this.

> Compile time programming ... aren’t in a standard yet.

There compile time programming story in C++ has been getting better and
better, and has made great strides with recent standards (check out constexpr,
which arrived in C++11).

> template meta programming is an agony to use.

Many people would agree with you, but it’s a matter of taste :). It’s “just
functional programming” (with an arguably terrible syntax).

I’m very jealous of many rust features, like built in algebraic data types,
pattern matching, but most of all their build and package stories.

~~~
ilovecaching
I'm curious, what would it take to get you to completely ditch C++ in favor of
Rust?

~~~
tomnj
Completely? I’m not sure. I need to try more Rust. But not having to write
another CMakeList sounds pretty good for starters :)

------
ilovecaching
Rust makes things that are technically possible to do in C++, easy to do.

Rust makes things that are painful to do in C++, pleasant to do.

Rust takes your template compiler errors, and gives you beautiful type
parameter errors with underlines.

Rust takes your scary macro and turns it into a sanitary macro.

Rust takes your crazy diamond of death and towers of inheritance, and gives
you traits, associated types, and composition.

Rust takes your runtime variant and insane union and gives you real sum types.

Rust takes your ASAN, Valgrind, debug alloc builds, and hopes and prayers, and
gives you a compiler that let's you use memory safely.

Rust is like C++ without the jagged edges, the specification no one
understands, removes the dangerous bits that no one person can keep in their
head, and trades the useless abstractions for nice functional abstractions
that actually are an improvement over writing C.

------
mastax
I'm a big fan of Rust, and seeing that Carmack writes it gives me warm fuzzy
feelings.

But this link is mostly content-free, and I don't think it's the type of link
that inspires useful discussion. I wish that HN had downvotes on posts, though
I suppose it uses several other methods.

~~~
yodon
Reading your comment 42 minutes later your fear that it would not inspire
useful discussion appears to be demonstrably unfounded.

Flagging a post out of fear of what might be said in response to the article
is the aspect of HN culture I find least admirable. Flagging a post is not the
same as downvoting. Flagging amounts to "I believe this article should not be
read by anyone from HN" because a fully flagged Article (flagged by more than
one person) removes the source link from view. That's a pretty serious
statement to feel you can make on behalf of all the other readers of HN, and
should be used only with the greatest of care.

------
noobiemcfoob
What is meant by people look to other languages more often because you can do
something in C++?

/My rephrasing, but I can't parse Carmack's point

~~~
lemmsjid
I believe his sentiment was that you can do too much in C++, so you pick other
languages because of restrictions they impose, or features they lack.

~~~
twic
I would imagine that Carmack specifically means that you can do unhelpful
things, like dereference null pointers, access uninitialised memory,
accidentally invoke undefined behaviour, etc. A foundational goal of Rust is
to make those things nigh-on impossible.

------
bsurmanski
For anyone else looking into Rust, I've been experimenting in it for the past
month. Heres my thoughts:

It advertises itself as a systems level language for the new age, but to me it
feels like a functional language in disguise. The borrow checker enforces an
unchained functional style (You may have 1 unique mutable reference or may
have many immutable references). This sounds bad if you're used to pointer
slinging, but once you get used to the peculiarities I find it forces me to
write better quality code.

The package management is the most refreshing feature. It's a pain to rewrite
my personal libraries in each language I use, but I found that most of the
things I needed were already available at higher quality than I would have
done. (And using them is a single config line).

For a 'low level language' they make it pretty hard to do some 'low level'
things. Example's I've found include:

* writing a doubley linked-list. Consensus is "don't use linked-lists. The stdlib has better tools

* Reading a file into a struct. They make it surprisingly difficult to say "hey, read these 10 bytes, its this struct". Consensus is to use more structured file formats like json or protobufs. There's libraries for reading those things.

Error handling is similar in style to Go, but it get rid of a lot of the
boilerplate. The '?' operator effectively acts as if err return err.

The macro system is really nice. I don't write many macros, but it does mean I
can use other people's powerful macros. My favorite are 'include_bytes!',
'lazy_static!' and 'dbg!'. The new procedural macros are pretty wacky,
essentially allowing you to parse or rewrite the AST. A powerful example I've
seen is static checks; This [0] example writes a compile time check to assert
that structs do not contain a member named 'bees'.

Overall it's been a fun language to mess around with.

[0] - [https://tinkering.xyz/introduction-to-proc-
macros/](https://tinkering.xyz/introduction-to-proc-macros/)

~~~
SamReidHughes
Maybe I'd call Rust a "resource-constrained" language than low-level. The
usecase being you want no GC, for memory inflation or CPU pauses, but suffer a
bit in overall CPU time and memory locality.

~~~
wtetzner
Why do you think you'd suffer in CPU time and memory locality?

~~~
SamReidHughes
Sometimes you have to put stuff behind pointers, use runtime checks for
safety, or change how you represent data to something suboptimal.

~~~
wtetzner
Sure, but the inverse is true too. Sometimes you can get _better_ performance
in Rust, because the borrow checker allows you to do things you would never do
in C/C++. For example, passing around array slices is very common in Rust, but
the cases where you'd do it in C or C++ is much more limited, because it's so
error prone. You have no guarantees that the memory being pointed to won't be
pulled out from under your feet.

~~~
SamReidHughes
I've had Rust hinder my ability to pass around array slices more than help.

~~~
wtetzner
The point is that while it's easy to do in C++, it's extremely error prone.

Rust encourages you to write code that works with things like slices and
references instead of copying, because the compiler won't let you use them in
a way that is error prone.

~~~
SamReidHughes
That's some nice sounding evangelism, but also completely besides the point.
You seem to think I don't understand C++ and Rust. Believe me, I do. Rust
inhibits use cases of array slicing that are both useful and not error prone.

Last time I read a blog post about this, they picked a situation that was
perfectly safe and ordinary in C++, with straightforward function-local safety
reasoning.

~~~
wtetzner
Just out of curiosity, do you have an example where Rust got in your way when
trying to use an array slice?

~~~
SamReidHughes
Approximately, here is one example. I wanted to use something like a
Vec<&[u8]> as a means of passing a set of buffers to a function. Likewise it
made sense to return such a value from another function, g. The way g worked
was to read or hold a large buffer into memory, e.g. 1 megabyte in size, and
then parse out the slices from it. So maybe you'd want to return something
that looks like the C++ type

    
    
        struct Foo {
        private:
            vector<uint8_t> buf;
        public:
            // points into buf
            vector<pair<const uint8_t *, size_t>> slices;
        };
    

Well you can't do that. Obviously there are workarounds, like to return an
object of type Foo holding the buf, with an api like impl Foo { fn
getSlices(&self) -> Vec<&[u8]> }. Internally the object holds a Vec<(usize,
usize)> or something like that, and you have a bunch of translation logic
around your API's. And extra work to allocate the return value. So that's one
example of Rust getting in your way.

Probably some others would be uses of Interval<Buf>, and some cases where
functions return Buf in
[https://github.com/srh/nihdb](https://github.com/srh/nihdb) . I wanted to use
&[u8] to represent the bounds of intervals, and IIRC that generally involved
passing intervals upwards into functions, but for some reason I can't remember
it got annoying and I couldn't be bothered to do it.

~~~
steveklabnik
This is a pain point, but it’s not due to slices; it’s the “self-referential
struct problem.” There are various solutions, as you note, but it can be
annoying, it’s true. It’s a tough one; in the general case, it’s saving you
from problems, but when you know you’re not going to hit those edge cases,
it’s less than ideal.

~~~
wtetzner
Is there a theoretical reason for the "self-referential struct problem", or is
it just an artifact of the current borrow-checker implementation?

It doesn't seem unsafe to have a struct field refer to another member of that
struct, but maybe I'm missing something.

~~~
steveklabnik
It's not theoretical, it's practical. Here's some code with a self-referential
struct:

[https://play.rust-
lang.org/?version=stable&mode=debug&editio...](https://play.rust-
lang.org/?version=stable&mode=debug&edition=2018&gist=13c75bfc3ab953c4126dfb86fe8f701b)

Here, we have a self-referential struct. If you run this, you may get
different numbers than me, but

    
    
      [src/main.rs:17] &f = Foo {
          x: 5,
          p: 0x00007ffcbbba41c0
      }
    

Here, p points to x. It's all good. The address of f is

    
    
      [src/main.rs:19] &f as *const Foo = 0x00007ffcbbba41b8
    

We move f into oh_no. Its address changes:

    
    
      [src/main.rs:25] &f as *const Foo = 0x00007ffcbbba3f28
    

... but p does not:

    
    
      [src/main.rs:26] &f = Foo {
          x: 5,
          p: 0x00007ffcbbba41c0
      }
    

Any access of p is now a use-after-free.

Does that make sense?

~~~
wtetzner
OK, so the issue is that references aren't updated when a move occurs. That
does make sense.

So to make this work, references would need to be re-written when a move/copy
occurs.

I can still see having an easy way to construct self-referential structs being
a useful thing, even if the compiler prevents you from moving them. Maybe with
a smart clone() method that can update references correctly.

However, I am a little confused about this specific example. I don't
understand why oh_no() taking ownership causes f to be copied to a new
location. Shouldn't it remain in the same place on the stack? I feel like I'm
missing something.

~~~
steveklabnik
> So to make this work, references would need to be re-written when a
> move/copy occurs.

Yes! C++ has a concept called "move constructors" that allows for this (this
would be that "smart clone" you talk about later in the comment), but we made
a decision to not include it. This introduces some nice properties, at the
cost of disallowing self-referencing structs.

> I can still see having an easy way to construct self-referential structs
> being a useful thing, even if the compiler prevents you from moving them.

So, in some sense, this is what the new Pin stuff is about. It lets you say
"from this point on, this thing isn't going to move again" and therefore be
self-referential.

> I don't understand why oh_no() taking ownership causes f to be copied to a
> new location.

"Taking owernship" means "move". "move" means "a memcpy from the old to the
new location." Rust isn't really special from any other sort of language with
value semantics here, other than disallowing you to use the old value, since
it was moved out from. Does that make sense?

~~~
wtetzner
So, in regards to why it's generally not safe to move a self-referential
struct, I understand now. My confusion about the memcpy is a tangent ;)

I guess my confusion is that I while realized "move" could mean a memcpy, I
didn't realize it _always_ meant a memcpy.

In other words, I thought the compiler would be smart enough to realized the
memcpy is unnecessary, even though ownership has been transferred as far as
the type system/borrow checker is concerned.

As another example, if you have this situation:

    
    
        let x = 45;
        dbg!(x);
        let y = x;
        dbg!(y);
    

Is there really any reason to allocate separate memory for y, if x is in
accessible after y is defined?

Or is this just an implementation detail, and could possibly change in the
future?

Also, thanks for being so responsive! I suppose this isn't really the best
place to have this discussion ;)

[EDIT] I realize that's not a good example, because 45 will just be copied.
But you get what I was trying to show.

~~~
steveklabnik
Semantically, it’s always a memcpy. In practice, the copy can be elided
depending on circumstance. But that’s an optimization, not the semantic.

(And yeah, Copy types are like that too; the only semantic difference between
Copy and move is if you can use the old binding.)

Any time! And yeah, the rust forums are probably better but it’s no big deal
:)

~~~
wtetzner
OK, cool, so in this case it's just that the Rust compiler isn't optimizing
away the memcpy. Thanks!

------
vertline3
As soon as I saw it was just a quote, I had this gut instinct it would be
Carmack speaking. The reason is because when DirectX was really marketing
hard, Carmack did some work in OpenGL and stood up for it. It was kind of
important to me since it was open.

------
inherentFloyd
I have never done anything in Rust. Are there any benefits? Any drawbacks?

~~~
gnuvince
If you don't mind a bit of a re-education, Rust allows you to write programs
that are as fast as those written in C or C++ and you'll be more confident
that they don't have strange memory bugs. (Reason: the ownership model of Rust
prevents a lot of bugs that are found in C or C++, but this model also rejects
many correct programs, so you need to re-learn how to write some programs.)

~~~
wwright
One thing people (like me ) often say is that it forces you to “learn to write
better.”

One way you can think of it is this:

\- Rust rejects some programs that are truly wrong (`“2” + 3` style)

\- Rust rejects some programs that really are just fine. They are actively
improving this with stuff like “Non-lexical lifetimes.”

\- Rust rejects some programs that are fine _how they are now_, but which are
hard to keep fine when you change them. This is part of why linked lists are
hard to write in Rust :-)

~~~
gnuvince
I admit that I'm very unexcited about non-lexical lifetimes—the cost-to-
benefit ratio seems worse than the current model. The current lifetime system
is simple to understand: a value is live until its owning variable goes out of
scope. It can be a bit restrictive sometimes (though it seems that after a
while you learn how to work around those issues easily enough), but it's easy
to keep in your head and reason about code. With non-lexical lifetimes, I
think that we'll have a harder time understanding lifetimes, it'll be more
difficult to teach them and to debug them.

~~~
wwright
My understanding is that the semantics are exactly the same, but it’s just
possible for the checker to match your intent more tightly.

For example, in lexical lifetimes, this fails:

    
    
        let x = &mut foo;
        
        if x.bar() {
            baz()
        } else {
            foo.consume() // takes foo by value
        }
    

It will complain that you’ve used `foo` while it is borrowed by `x`, even
though a human can easily tell that `x` is already irrelevant by that point.
NLL would accept this (I believe).

