The C++ way to relocate object of implicitly non-movable class
Asked Answered
P

1

6

I'm writing some custom library like stl, but without allocations in ctors and with disabled copy ctors in resource owning classes (because the environment doesn't support exceptions and all allocs on the heap needs to be checked by retcode).

So I'm porting btree from https://github.com/Kronuz/cpp-btree/, and got stuck because of problem of code in conjunction with my approach.

value_type, as in all stl implementations, is std::pair<const Key, Value>. const qualifier makes the whole pair implicitly non-movable.

So code

x->construct_value(j, std::move(fields_.values[i]));

(https://github.com/Kronuz/cpp-btree/blob/35ac0ec96f1fca1463765f169390059ab82d3aac/btree/btree.h#L615)

in fact doesn't move object (returns T& instead of T&&) and

new (v) value_type(std::forward<Args>(args)...);

(https://github.com/Kronuz/cpp-btree/blob/35ac0ec96f1fca1463765f169390059ab82d3aac/btree/btree.h#L883)

is rightly unable to construct pair by copy ctor.

Is there a way to relocate object in memory without or bypassing copy-move semantic? Of course, the simple workaround is to make std::pair<Key, Value> with mutable key, but it's not exactly the same.

I found "trivially_realocable" proposal by Arthur O'Dwyer, and I made conclusion that it's exactly about the case. (https://quuxplusone.github.io/blog/2018/07/18/announcing-trivially-relocatable/)

Prejudge answered 22/4, 2020 at 12:37 Comment(1)
Obviously, no. You either respect the language's semantics for copy/move, or you go outside the language. You could solve it in the standard computer science way: Add a layer of indirection. 1) Have the container keys be pointers to your real keys. 2) Change your resource holding class to be a handle (i.e., opaque pointer) to instances holding the actual restricted resource. (Actually, those two are the same, just in case #2 you give a name to the handle and not just use naked pointers.)Bathsheeb
A
5

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.

Adulteration answered 16/7, 2020 at 17:11 Comment(1)
"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" Um... you actually can do that. For some reason, the trivial copyability rules only require that at least one of the copy/move constructors/assignment operators is trivial; the others can be deleted and the type will still be trivially copyable.Baptiste

© 2022 - 2024 — McMap. All rights reserved.