Document Number: P0XXXR0
Date: 2016-xx-xx
Reply-to: tongari95@gmail.com
Audience: EWG
This proposal introduces the concept of object transition - a way to transit the object liftetime - into the core language.
Rvalue-reference introduced a nice way in the C++ type system for move-semantic. While move operations could be much cheaper than their copy counterparts, they're still suboptimal in many common places.
A move operation typically involves 3 phases:
- Copy the underlying data from the source
- Reset the data of the moved-from object
- Destruct the moved-from object afterward
Strictly speaking, the last phase is not part of a move operation, but it's an involved operation nevertheless due to the rule of object liftetime.
Because move-semantic is designed for broader usage, it doesn't make the moved-from object in detructed state, for example:
T a{...};
T b{std::move(a)};
a = f(...); // `a` can be reused here
Instead, the moved-from object is left in a "valid but unspecified state" after the move, so the liftetime of a
continues and it can still be used from the user code.
However, in many cases (if not most), we don't use a
anymore after it's moved, so such a valid-state guarantee becomes pure overhead.
And rember the C++ motto? You don't pay what you don't use. A more effiecent object transition should avoid the last 2 phases in a move operation.
That said, the benefit of object transition is not just for performance, it also helps:
- Varaible renaming for code readibilty
- Relaxed semantic for class invariant
which we're see in later examples.
We propose the following type notation to the C++ type system to denote object transition:
T'&
- Borrowing reference to TT'&&
- Transiting reference to TT'(T'&&)
- Revive constructor
Traditionally, the liftetime of an object in C++ is ended when its destructor is called. In this proposal, if an object is transited using the transit-operation, the liftetime of the transited object is not ended as the destructor is not called, instead, the liftetime continues on the new object.
template<class T>
constexpr T'&& transit(T'& t)
{
return static_cast<T'&&>(t);
}
template<class T>
constexpr T'&& claim(T& t)
{
return static_cast<T'&&>(t);
}
// Cannot claim the ownership from a borrowed one
template<class T>
void claim(T'& t) = delete;
// forward & move remain the same
T a;
{
T'& b = a; // borrow the ownership from `a`
b.doSomething();
a.doSomething(); // error, the ownership of `a` is not returned yet
// `b` return the ownership to `a`
}
a.doSomething(); // ok, the ownership is returned
{
T'&& b = std::transit(a); // transit the ownership from `a`
b.doSomething();
a.doSomething(); // error, the ownership of `a` is transited
// the lifetime of the bound object ends, i.e. b.~T() will be called
}
a.doSomething(); // error, cannot reference `a` anymore
template<class Ty>
void f(Ty&&);
T a, b;
f(T{}); // Ty = T'
f(a); // Ty = T&
f(std::move(a)); // Ty = T
f(std::transit(b)); // Ty = T'
There are 3 ownership levels - 0, 1, 2.
- 0 - No ownership
- 1 - On a variable with auto-storage or is an owning reference
- 2 - On a owning reference in a revive constructor with the same token
struct Derived : Base
{
T val;
Derived'(Derived'&& other) // other has ownership level 2
: Base(std::transit(other)), val(std::transit(other).val)
{
f(std::transit(other)); // error, cannot transit from ownership level 2
}
};