Hacker News new | ask | show | jobs
by klodolph 2613 days ago
Rust's borrow checking does not correspond to RAII. A simplified explanation is that borrow checking gives you the guarantee that mutable references are unique and immutable references do not change.

Rust's lifetime system is vaguely like RAII, but the C++ type system gives you no way to create an object with lifetimes that don't correspond to some scope. In Rust this is done by moving values, but in C++ this is not possible, you have to fake it by using std::move(), which really just creates an rvalue reference.

One of the big things that causes problems in C++ is iterator invalidation. This is not solved with RAII, but it is solved in Rust with the borrow checker. The price you pay is that iterators in Rust are strictly less powerful than iterators in C++, because the way C++ iterators work cannot really be expressed in the Rust type system. (In short, C++ lets you have as many iterators as you like into the same container, and defines ranges as pairs of iterators. Some algorithms are more naturally expressed this way.)

2 comments

> C++ lets you have as many iterators as you like into the same container

While it's true that Rust doesn't let you have multiple mutable iterators, it's worth pointing out that you can certainly have multiple immutable iterators.

    let items = &[42, 101];

    // All products (a*a, a*b, b*a, b*b)
    for x in items {
      for y in items {
        println!("{}", x * y);
      }
    }

    println!("");

    // Pairwise products (a*a, b*b)
    let pairs = Iterator::zip(items.iter(), items.iter());
    let pointwise_products = pairs.map(|(x, y)| x * y);
    for product in pointwise_products {
      println!("{}", product);
    }
That's really not what I'm talking about. In your example you are using multiple iterators to iterate over the same structure multiple times, but in C++ you can use pairs of iterators to represent ranges. For example, I can have three iterators i, j, and k, which represent two ranges: i..j and j..k.

Even if i, j, k are const iterators, it's not pleasant to translate this to Rust. So it's not an issue of whether Rust lets you have multiple iterators or not, the issue is that Rust iterators are strictly less expressive than C++ iterators.

And that's okay. It's a tradeoff.

For more information about how iterators are used in C++, I would refer to the <algorithms> portion of the standard library. https://en.cppreference.com/w/cpp/algorithm

Ah, I understand. You're referring to how iterators in C++ are generalized indices into their collections. Certainly, Rust iterators have a more constrained purpose.

I personally prefer having separate constructs for iteration and indexing, so I think it's a matter of taste.

Is that different from something like this?

  fn main() {
      let list = &[0, 1, 2, 3, 4, 5, 6, 7, 8];
      let i = 0;
      let j = 4;
      let k = 8;
      let iter_ij = list[i ..= j].iter();
      let iter_jk = list[j ..= k].iter();
      let pairs = Iterator::zip(iter_ij, iter_jk);
      for (a, b) in pairs {
          println!("{}, {}", a, b);
      }
  }
Yes, that is different, because in C++ you can move j forward and backwards.
Ahh, I see, thanks for clarifying. Rust's Iterators are similar to C++ ForwardIterators. It seems C++'s default is BidirectionalIterator.
Even C++ ForwardIterators are more powerful, because you can move them independently. Rust lets you move the beginning of the range forwards, and maybe the end of the range backwards. With ForwardIterator you would be able to move the end of the range forwards, making the range longer.
> but the C++ type system gives you no way to create an object with lifetimes that don't correspond to some scope.

Counterexample: a data member of a class has a lifetime tied to the lifetime of the containing object instance, rather than to a lexical scope.

You are just using the same storage duration as the containing object instance, and the C++ type system only gives you any guarantees for automatic, static, and thread storage duration. These all correspond directly to lexical scopes. The type system does not have a way to express dynamic storage duration, but Rust does because Rust has move semantics in the type system. C++ has move semantics, but they are runtime semantics built into the implementation of the library and not part of the type system, so you get no compile-time checking that you used move semantics correctly.

Phrased another way, I'm just saying that the C++ type system only expresses two types of storage durations: those tied to lexical scopes and dynamic storage durations, and provides no checks for dynamic storage durations.