Trivially Relocatable versus Destructive Movable
I presented P1144 “Object relocation in terms of move plus destroy” at the SG14 working meeting
on 2018-09-26. I was also pleasantly surprised by the number of shout-outs the idea received at
CppCon in general — including in Mark Elendt’s keynote.
(The specific shout-out to “std::trivially_relocatable
in the pipeline” is at 42m00s.
I did talk with Mark later on and point out that “in the pipeline” is an extremely generous
way to describe the situation!)
This evening I had a very productive conversation with Pablo Halpern, in which I learned that my P1144 is not actually as close to Pablo’s N4158 “Destructive Move rev. 1” (October 2014) as I had been thinking and saying — and I believe Pablo learned that he had been similarly misinterpreting P1144! Our papers actually have a significant theoretical difference.
The major major difference between N4158 and P1144 comes down to this question:
Can I define my own “relocation” operation, to be used by
vector::resize
and so on, which is not quitememcpy
but also more efficient than move-plus-destroy?
My P1144 says, definitively, no.
Pablo Halpern’s N4158 says, definitively, yes.
N4158 proposes that there should be an ADL customization point
which Pablo named uninitialized_destructive_move(T *from, T *to)
(but whose natural name
in P1144-land would be simply relocate
). Any user could overload uninitialized_destructive_move
for their own type. Pablo’s N4158 strongly implies that vector::resize
et cetera
would be respecified to call this customization point.
For example, consider a class using something like the PIMPL idiom, with the class invariants that
pimpl
is never null and pimpl->me
is always this
, even for a moved-from object:
namespace My {
struct BlobImpl {
Blob *me;
explicit BlobImpl(Blob *me) : me(me) {}
};
struct Blob {
std::unique_ptr<BlobImpl> pimpl;
explicit Blob() {
pimpl = std::make_unique<BlobImpl>(this);
}
Blob(Blob&& rhs) : pimpl(std::move(rhs.pimpl)) {
pimpl->me = this;
rhs.pimpl = std::make_unique<BlobImpl>(&rhs);
}
Blob(const Blob&); // expensive
~Blob() = default;
};
} // namespace My
Pablo’s N4158 would have allowed the user to provide an ADL overload of
uninitialized_destructive_move
, like this:
struct Blob {
std::unique_ptr<BlobImpl> pimpl;
explicit Blob() {
pimpl = std::make_unique<BlobImpl>(this);
}
Blob(Blob&& rhs) : pimpl(std::move(rhs.pimpl)) {
pimpl->me = this;
rhs.pimpl = std::make_unique<BlobImpl>(&rhs);
}
Blob(const Blob&); // expensive
~Blob() = default;
friend void uninitialized_destructive_move(Blob *from, Blob *to) noexcept {
::new (to) Blob(from, Blob::DestructiveMoveTag{});
}
private:
struct DestructiveMoveTag {};
explicit Blob(Blob *rhs, DestructiveMoveTag) noexcept :
pimpl(std::move(rhs->pimpl))
{
pimpl->me = this;
rhs->~Blob();
}
};
Under N4158, trivial destructive-movability was an orthogonal concern; in the absence of
attributes, Pablo proposed that maybe the user could just specialize
std::is_trivially_destructive_movable
themselves. So then the library would provide
a fallback std::uninitialized_destructive_move
just like it provides a fallback std::swap
today:
namespace std {
template<class T> void uninitialized_destructive_move(T *from, T *to) {
if constexpr (std::is_trivially_destructive_movable_v<T>) {
// magic, tantamount to memcpy
} else {
::new (to) T(std::move(*from));
from->~T();
}
}
} // namespace std
And vector::resize
would do something like this:
if constexpr (std::is_nothrow_destructive_movable_v<T>) {
for (size_t i = 0; i < size_; ++i) {
using std::uninitialized_destructive_move;
uninitialized_destructive_move(olddata_ + i, newdata_ + i);
}
} else ...
Notice all the details above: the fact that vector
can use
uninitialized_destructive_move
only when it is nothrow; the fact that it still has to
be used in a loop; my complete ignoring of allocator-awareness concerns (which in fairness
are not hard to solve); and the low but non-zero potential for foot-shooting.
Bottom line, this sounds utterly complicated!
However, N4158’s customization-point approach does have a tangible benefit in one case. Observe that
static_assert(not std::is_nothrow_default_constructible_v<Blob>);
static_assert(not std::is_nothrow_move_constructible_v<Blob>); // !!
static_assert(std::is_nothrow_destructible_v<Blob>);
static_assert(std::is_nothrow_destructive_movable_v<Blob>); // N4158
The line marked // !!
implies that by the definition in previous drafts of P1144,
static_assert(not std::is_nothrow_relocatable_v<Blob>);
But in fact the human programmer does know how to relocate a Blob
in a noexcept manner!
P1144 doesn’t provide any mechanism to share that information with vector::resize
.
Under P1144, vector::resize
will merely see that Blob
is non-nothrow-move-constructible
and fall back to the expensive copy constructor.
Under Pablo’s N4158, vector::resize
would have used the fast nothrow ADL overload of
uninitialized_destructive_move
rather than the expensive copy constructor.
I believe that this particular part of N4158 is orthogonal to P1144’s concept of “trivial” relocatability,
and theoretically could be added later (as a pure library extension) if the committee wanted it.
I’m even ambivalent now as to whether I should remove the trait is_nothrow_relocatable_v
from my proposal.
But one thing I know is that calling an ADL uninitialized_destructive_move
(or an ADL relocate
)
in a loop will never give optimal codegen for the trivial case.
The implementor of std::vector
or folly::FBVector
must be given permission to use memcpy
on a whole range of trivially relocatable objects — no loop, no constructor calls, just a single
simple call to the library memcpy
. As Mark Elendt’s keynote session quite visibly showed,
that’s what they’re already doing today.