'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);
}
}
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 |