'Why can I push items into a Vec I am iterating over with a while loop, but not with a for loop?

Like the rust code below: the while loop compiles and runs fine, but for iter version does not compile, due to error:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  --> src/main.rs:22:9
   |
20 |     for i in v.iter() {
   |              --------
   |              |
   |              immutable borrow occurs here
   |              immutable borrow later used here
21 |         println!("v[i]: {}", i);
22 |         v.push(20);
   |         ^^^^^^^^^^ mutable borrow occurs here

error: aborting due to previous error

But as understood, while loop also has the same scenario, len and get also borrow immutably, why it does not conflict with push as borrow mutably? Please advise what is my understanding missing here, thank you so much for the enlightening!

fn main() {
    let mut v = Vec::new();

    v.push(1);
    v.push(2);
    v.push(3);
    v.push(4);

    let mut i = 0;

    while i < v.len() && i < 10 {
        v.push(20);
        println!("v[i]: {:?}", v.get(i));
        i += 1;
    }

    // for i in v.iter() {
    //     println!("v[i]: {}", i);
    //     v.push(20);
    // }
}


Solution 1:[1]

The for version of your code is roughly equivalent to the following:

fn main() {
    let mut v = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);
    v.push(4);
    
    let mut it = v.iter();
    while let Some(i) = it.next() {
        println!("v[i]: {}", i);
        v.push(20);
    }
}

Playground

If you try to compile that you will get an error that maybe makes a bit more sense:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  --> src/main.rs:11:9
   |
8  |     let mut it = v.iter();
   |                  - immutable borrow occurs here
9  |     while let Some(i) = it.next() {
   |                         -- immutable borrow later used here
10 |         println!("v[i]: {}", i);
11 |         v.push(20);
   |         ^^^^^^^^^^ mutable borrow occurs here

The iterator is borrowing v immutably for the entire duration of the loop, thus you cannot take any mutable borrows within the loop.

Of course, even if you could do that, you would end up with an infinite loop because you keep appending another item.

Solution 2:[2]

Simply put, when you call .iter(), you create a new objects (an iterator), which borrows your vector (immutably), and the gives the element one by one, which means you actually borrow v the whole time of the loop. On the other hand, when you access it via .get(i) you directly borrow one element at the time from the vector, and so it is freed from borrowing restrictions when you push.

The reason for such restriction is very simple: imagine your actual for loop did compile, it would run forever (and to prevent this in the while loop, you had to add the artificial condition i<10!), whereas this is clearly not the intended goal (or if it would be you clearly would do it otherwise, for example with a while let or loop statement), and Rust tries to prevent you “shooting yourself in the leg” because you don't really how to do what you want to do, and try the wrong way.

To do what you wanted to do, you could do:

for i in 0..v.len() {
    v.push(20)
}

because the v.len() call does not borrow v for the time of the whole for loop, but only at the beginning.

Solution 3:[3]

The Rust borrow checker defines a set of rules as to what can be borrowed in which possible modes (mutable or immutable). It does even more, namely deal with life times and if you don't borrow, you move.

For aspiring Rust programmers, dealing with the rules, imposed by the borrow checker is the valley of pain (aka the learning curve), they have to get through to become productive.

Once, the iterator borrows the vector immutably (and holds it for the duration of the loop), the borrow checker complains about attempts to mutate.

If you are a bit of a lateral thinker, like I am... whenever I see something in a new language I cannot quite wrap my head around, I try to write an educational approximation of that "strange thing" in a language I know better already.

So, in the hope it helps, you can find a very simplified and daring approximation of what rust does (at compile time) in C++. Only - in my C++ code, it happens at runtime and violation to the rules result in an exception being thrown.

The "state" of the instance we work with (a std::vector<int32_t> in this case), regarding mutability and what we still can do according to our hastily invented set of borrow rules (which might or might not be similar to rusts) is expressed by distinct types (Managed<T>::ConstRef or Managed<T>::MutableRef). And the scope, in which the state applies is the scope of the lambda functions, which serve as "smart scopes". main() in the code below is trying to replicate the for i in vec.iter() { .. } scenario.

Maybe looking at the problem from this angle is helpful to someone.

#include <iostream>
#include <cstdint>
#include <vector>
#include <string>
#include <stdexcept>
#include <sstream>

template<class T>
void notNull(const T* p)
{
  if(nullptr == p)
    throw std::invalid_argument("pointer is null!");
};

enum class Demand : uint8_t
{ CONST
, MUTABLE
};

inline
std::string
exinfo
(const char* message
, const char * file
, int line
 )
{
  std::ostringstream os;
  os << file << ":" << line << std::endl
     << message << std::endl;
  return os.str();
}

class borrow_error
  : public std::logic_error
{
public:
  explicit borrow_error(const std::string& what_arg)
    : logic_error(what_arg)
  {}
  explicit borrow_error(const char* what_arg)
    : logic_error(what_arg)
  {}
};
class mutable_borrow_after_const_borrow
  : public borrow_error
{
public:
  mutable_borrow_after_const_borrow(const char* file, int line)
    : borrow_error
      (exinfo("mutable borrow after const borrow",file,line))
  {}
};

#define THROW_MUTABLE_BORROW_AFTER_CONST_BORROW \
  throw mutable_borrow_after_const_borrow(__FILE__,__LINE__)


class const_borrow_after_mutable_borrow
  :public borrow_error
{
public:
  const_borrow_after_mutable_borrow(const char* file, int line)
    : borrow_error
      (exinfo("const borrow after mutable borrow",file,line))
  {}
};

#define THROW_CONST_BORROW_AFTER_MUTABLE_BORROW \
  throw const_borrow_after_mutable_borrow(__FILE__,__LINE__)

class multiple_mutable_borrows
  :public borrow_error
{
public:
  multiple_mutable_borrows(const char* file, int line)
    : borrow_error
      (exinfo("more than one mutable borrow",file,line))
  {}
};

#define THROW_MULTIPLE_MUTABLE_BORROWS \
  throw multiple_mutable_borrows(__FILE__,__LINE__)

class mutable_access_to_const_attempt
  : public borrow_error
{
public:
  mutable_access_to_const_attempt(const char* file, int line)
    : borrow_error
      (exinfo("request to mutate the immutable.",file,line))
  {}
};

#define THROW_MUTABLE_ACCESS_TO_CONST_ATTEMPT \
  throw  mutable_access_to_const_attempt(__FILE__,__LINE__)


template <class T>
struct Managed
{
  using this_type = Managed<T>;
  using managed_type = T;
  struct ConstRef
  {
    this_type * origin;
    ConstRef(this_type *org, const char* file, int line)
      : origin{org}
    {
      notNull(origin);
      // if( 0 != origin->mutableAccess )
      // {
      //    throw const_borrow_after_mutable_borrow(file,line);
      // }
      origin->constAccess++;
    }
    ~ConstRef()
    {
      origin->constAccess--;
    }
    operator const T&()
    {
      return origin->instance;
    }
  };
  struct MutableRef
  {
    this_type *origin;
    MutableRef(this_type *org, const char* file, int line)
      : origin{org}
    {
      notNull(origin);
      if(origin->instanceMode == Demand::CONST)
      {
    throw mutable_access_to_const_attempt(file,line);
      }
      if( 0 != origin->constAccess )
      {
    throw mutable_borrow_after_const_borrow(file,line);
      }
      // also allow max 1 mutator
      if( 0 != origin->mutableAccess )
      {
    throw multiple_mutable_borrows(file,line);
      }
      origin->mutableAccess++;
    }
    ~MutableRef()
    {
      origin->mutableAccess--;
    }
    operator T&()
    {
      return origin->instance;
    }
  };
  
  Demand instanceMode;
  int32_t constAccess;
  int32_t mutableAccess;
  T& instance;
  Managed(T& inst, Demand demand = Demand::CONST)
    : instanceMode{demand}
    , constAccess{0}
    , mutableAccess{0}
    , instance{inst}
  {
  }
};

template <typename T, class F>
auto
borrow_const
( T& instance
, F body
, const char * file
, int line
) -> void
{
  typename T::ConstRef arg{&instance, file, line};
  body(arg);
}

#define BORROW_CONST(inst,body) \
  borrow_const((inst),(body),__FILE__,__LINE__)

template <typename T, class F>
auto
borrow_mut
( T& instance
, F body
, const char * file
, int line
) -> void
{
  typename T::MutableRef arg{&instance, file, line};
  body(arg);
};

#define BORROW_MUT(inst,body) \
  borrow_mut((inst),(body),__FILE__,__LINE__)


using VecI32 = std::vector<int32_t>;
using ManagedVector = Managed<VecI32>;

int main(int argc, const char *argv[])
{
  VecI32 myVector;
  ManagedVector vec{myVector,Demand::MUTABLE};

  try
  {
    BORROW_MUT
      ( vec
      , [] (ManagedVector::MutableRef& vecRef) -> void
    {
      static_cast<VecI32&>(vecRef).push_back(1);
      static_cast<VecI32&>(vecRef).push_back(2);
      // during iteration, changing targets are bad...
      // if you borrow the vector to an iterator,
      // the "state" of the vector becomes 'immutable'.
      BORROW_CONST
        ( *(vecRef.origin)
        , [] (typename ManagedVector::ConstRef& vecRef) -> void
          {
        for( auto i : static_cast<const VecI32&>(vecRef) )
        {
          std::cout << i << std::endl;
          // Enforced by the rust compiler established
          // borrow rules,
          // it is not allowed to borrow mut from something
          // immutable.
          // Thus, trying to change what we iterate over...
          // should be prevented by the borrow checker.
          BORROW_MUT
            ( *(vecRef.origin)
            , [] (typename ManagedVector::MutableRef& vecRef)
                 -> void
              {
            // next line should throw!
            // but our BORROW_MUT throws first.
            static_cast<VecI32&>(vecRef).push_back(3); 
              });
        }
          });
    });
  }
  catch(borrow_error& berr)
  {
    std::cout << "borrow error: " << berr.what() << std::endl;
  }
  catch(std::logic_error& lerr)
  {
    std::cout << "logic error: " << lerr.what() << std::endl;
  }
  catch(std::exception& ex)
  {
    std::cout << "std::exception: " << ex.what() << std::endl;
  }
  catch(...)
  {
    std::cout << "We are taking horrible, HORRIBLE damage!" << std::endl;
  }
  
  return 0;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Herohtar
Solution 2 BlackBeans
Solution 3 BitTickler