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