The C++ working draft now has a definition of replaceable type (is_replaceable<T>
), which it got from the just-adopted P2786R13. P2786R13 itself says:
Replaceability is a semantic property of a type, where move assignment is isomorphic to destroy then move-construct.
libc++ is thinking about adding that trait and trying to gate optimizations on it, but they're running into confusion about what this should actually mean in practice. Is shared_ptr<int>
"replaceable"? After all, it seems to be not true that you could replace
void f(shared_ptr<int>& a, shared_ptr<int>& b) {
a = std::move(b);
}
with
void f(shared_ptr<int>& a, shared_ptr<int>& b) {
a.~shared_ptr();
::new (&a) shared_ptr<int>(std::move(b));
}
because if b
's lifetime is controlled by a
, then (1) the refcount of a
would drop to zero when it shouldn't, and (2) b
would be accessed outside its lifetime. Now, self-move-assignment is UB for all library types [EDIT: not anymore], but assignment in a general situation like this is not supposed to be UB, AFAIK.
struct S {
int i_;
std::shared_ptr<int> p_;
};
int main() {
std::shared_ptr<S> oa = std::make_shared<S>();
std::shared_ptr<int> a(oa, &oa->i_);
std::shared_ptr<int>& b = oa->p_;
b = std::make_shared<int>(42);
oa = nullptr;
assert(a.use_count() == 1);
assert(b.use_count() == 1);
f(a, b); // either move-assign (well-defined), or destroy-and-construct (introduces UB)
printf("a.use_count() is %zu (should be 1)\n", a.use_count());
printf("*a is %d (should be 42)\n", *a);
}
The question of what is_replaceable
is supposed to mean in practice, is closely related to the question of what optimizations are intended to be gated on is_replaceable
.
Perhaps the intent is that nobody should ever actually write an optimization that replaces assignment with destroy-and-construct; they should only ever gate on the conjunction of is_replaceable && is_trivially_relocatable
(i.e. the P1144 trait that library vendors were asking for). In that case, it would be "safe" for libc++ to mark its shared_ptr
as is_replaceable
; it would just fall on the user's shoulders to understand that this library trait doesn't mean quite what it says in the paper standard.
IIUC, the original hypothesis was that is_replaceable
would be useful for things like vector::erase
. But in fact — let V
be pmr::vector<int>
; then we want to optimize pmr::vector<V>::erase
, but is_replaceable_v<V>
is false. So is_replaceable
doesn't suffice to distinguish which types should get the insert
/erase
optimization, either; we still need to write a special case into vector
to handle PMR types specifically.
What can library vendors do with this new trait?