Arthur O'Dwyer here! :)
Indeed, there's no way to do what you want in standard C++. In fact, there's no way to do what you want in P1144 trivial-relocation–land, either. Essentially, you've got a type like this:
using Trouble = std::pair<const std::unique_ptr<int>, int>;
which is not copyable (because unique_ptr
is not copyable) but also not movable (because const unique_ptr
is not move-from-able).
The operation you want to perform on it is called "relocation." You have named it move_value
, but you should go right now and rename it to relocate_value
so that the reader of your code knows that you don't mean "move" in the C++ sense; you mean "relocate"!
In P1144-land, "relocate" is literally a synonym for "move and then destroy the source." So P1144 considers your type Trouble
to be non-relocatable, because it is non-movable. P1144 doesn't try to introduce a new verb "relocate"; it simply provides a performant codepath for types that are already relocatable under the current standard when the operation happens to be trivial. It's impossible to make a type that is P1144-trivially-relocatable without also being movable-and-destructible, in the same way as it's impossible today to make a type that the compiler will recognize as "trivially copyable" without its also being copy-assignable. The existence of the individual component operations is a prerequisite for the triviality trait. (I hope that helps explain why P1144 does it that way!)
To reiterate: You can't solve your problem without going outside of the standard.
So, how would I design a solution to your problem?
Step one, I'd write the client code the best way I knew how:
// Relocate value i in this node to value j in node x.
void relocate_value(int i, btree_node* x, int j) {
assert(x != this || i != j);
assert(0 <= i && i <= fields_.count);
assert(0 <= j && j <= x->fields_.count);
if constexpr (my::is_trivially_relocatable_v<value_type>) {
memcpy(&x->fields_.values[j], &fields_.values[i], sizeof(value_type));
} else {
::new (&x->fields_.values[j]) value_type(std::move(fields_.values[i]));
fields_.values[i].~value_type();
}
}
Notice that I've inlined the x->construct_value
and destroy_value
stuff here. In real life I might factor this stuff back out into a helper. P1144 names this helper std::relocate_at
:
// Relocate value i in this node to value j in node x.
void relocate_value(int i, btree_node* x, int j) {
assert(x != this || i != j);
assert(0 <= i && i <= fields_.count);
assert(0 <= j && j <= x->fields_.count);
my::relocate_at(&fields_.values[i], &x->fields_.values[j]);
}
Okay, that's the client's side of the situation. Now what do we do on the warrantor's side? In P1144-land, the compiler takes care of figuring out which types are trivially relocatable. But (A) we don't have P1144's compiler support, and (B) we actually want a slightly different definition anyway, because we want const unique_ptr
to be considered trivially relocatable. So we'll make up a customization point that type-authors can customize:
template<class T, class = void>
struct is_trivially_relocatable : std::is_trivially_copyable<T> {};
template<class T>
inline constexpr bool is_trivially_relocatable_v = is_trivially_relocatable<T>::value;
Notice the extra parameter for "This one weird trick for customization by template (partial) specialization" (of the class template, not of the variable template). This lets us write customizations like
template<class A, class B, class = std::enable_if_t<my::is_trivially_relocatable_v<std::remove_const_t<A>> && my::is_trivially_relocatable_v<std::remove_const_t<B>>>>
struct is_trivially_relocatable<std::pair<A, B>> : std::true_type {};
The practical engineering concern here is: Whose responsibility is it to define the specialization(s) of my::is_trivially_relocatable
for standard library types such as std::pair
etc.? If both you and your coworker define specializations for the same type, you have an ill-formed program.
Furthermore, professional coders have proven very bad at guessing whether complicated STL types such as std::string
and std::list
are trivially relocatable in practice. So giving you and your coworker this power is really asking for subtle bugs all over the place. (See Folly issue 889, for example.)
Finally, if both you and your coworker forget to add a specialization for your trivially relocatable type Widget
(where Widget
happens to be movable and destructible as well), then your code will silently lack this optimization — which is definitely a downside for a lot of people. If we're going to permit this optimization, we ought to make sure that it happens automatically, and doesn't get pessimized just because someone misspelled a template specialization.
TLDR:
You called the operation move_value
but you should call it relocate_value
.
P1144 would not help your actual use-case with const unique_ptr
; it optimizes only types which are already movable in C++20, and const unique_ptr
is not movable.
You can simulate P1144 with a customization point. You'll be in good company (Facebook Folly, Bloomberg BSL, EASTL), but there are huge maintainability downsides to consider.