首页 > 解决方案 > C++ 重定位隐式不可移动类对象的方法

问题描述

我正在编写一些像 stl 这样的自定义库,但在 ctors 中没有分配,并且在资源拥有类中禁用了复制 ctors(因为环境不支持异常并且堆上的所有分配都需要通过 retcode 检查)。

所以我从https://github.com/Kronuz/cpp-btree/移植 btree ,由于代码问题与我的方法相结合而被卡住了。

value_type与所有 stl 实现一样,是std::pair< const Key, Value>const限定符使整个对隐式不可移动。

所以代码

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

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

实际上不移动对象(返回 T& 而不是 T&&)和

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

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

正确地无法通过复制 ctor 构造对。

有没有办法在没有或绕过复制移动语义的情况下重新定位内存中的对象?当然,简单的解决方法是使std::pair< Key , Value>具有可变键,但并不完全相同。

我找到了 Arthur O'Dwyer 提出的“trivially_realocable”提案,我得出的结论是,这正是这种情况。(https://quuxplusone.github.io/blog/2018/07/18/announcing-trivially-relocatable/

标签: c++movecopy-constructormove-semanticsmutable

解决方案


亚瑟奥德怀尔在这里!:)

事实上,在标准 C++ 中没有办法做你想做的事。事实上,在P1144 trivial-relocation –land 中也没有办法做你想做的事。本质上,你有一个像这样的类型:

using Trouble = std::pair<const std::unique_ptr<int>, int>;

它不可复制(因为unique_ptr不可复制)但也不可移动(因为const unique_ptr不可移动)。

您要对其执行的操作称为“重定位”。您已将其命名为move_value,但您现在应该将其重命名为 ,relocate_value以便您的代码读者知道您的意思不是 C++ 意义上的“移动”;你的意思是“搬家”!

在 P1144-land 中,“relocate”实际上是“move and then destroy source”的同义词。所以 P1144 认为你的类型Trouble是不可重定位的,因为它是不可移动的。P1144 不会尝试引入动词“relocate”;当操作碰巧是微不足道的时候,它只是为在当前标准下已经可重定位的类型提供了一个高性能的代码路径。不可能创建一个 P1144 可轻松重定位的类型而不是可移动和可破坏的类型,就像今天不可能创建一个编译器将识别为“普通可复制”的类型而不是它也被复制-可分配的。单个组件操作的存在是前提对于琐碎的特质。(我希望这有助于解释为什么 P1144 会这样做!)


重申一下:不超出标准就无法解决问题。

那么,我将如何为您的问题设计解决方案?

第一步,我会以我知道的最佳方式编写客户端代码:

// 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();
    }
}

请注意,我已在此处内联了x->construct_valueanddestroy_value内容。在现实生活中,我可能会将这些东西重新考虑为助手。P1144 将此助手命名为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]);
}

好的,这是客户的情况。现在我们在保证人方面做什么?在 P1144-land 中,编译器负责确定哪些类型可以轻松重定位。但是(A)我们没有 P1144 的编译器支持,并且(B)我们实际上想要一个稍微不同的定义,因为我们希望const unique_ptr被认为是可以重定位的。因此,我们将组成一个类型作者可以自定义的自定义点:

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;

请注意“通过模板(部分)专业化进行定制的这一奇怪技巧”(类模板,而不是变量模板)的额外参数。这让我们可以编写自定义内容,例如

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 {};

这里实际的工程问题是:定义my::is_trivially_relocatable标准库类型(例如std::pairetc.)的专业化是谁的责任?如果您和您的同事都为同一类型定义专业化,那么您的程序格式不正确。

此外,事实证明,专业编码人员非常不擅长猜测复杂的 STL 类型,例如std::stringstd::list在实践中是否可以轻松重定位。因此,赋予您和您的同事这种权力实际上是要求到处寻找细微的错误。(例如,参见Folly 问题 889。

最后,如果您和您的同事都忘记为您的可重定位类型WidgetWidget恰好也是可移动和可破坏的)添加专门化,那么您的代码将默默地缺乏这种优化——这对很多人来说绝对是一个缺点。如果我们要允许这种优化,我们应该确保它自动发生,并且不会因为有人拼错模板特化而感到悲观。


TLDR:

  • 您调用了该操作move_value,但您应该调用它relocate_value

  • P1144 对您的实际用例没有帮助const unique_ptr;它仅优化在 C++20 中已经可移动且不可const unique_ptr移动的类型。

  • 您可以使用自定义点模拟 P1144。你会在好公司(Facebook Folly、Bloomberg BSL、EASTL),但要考虑可维护性的巨大缺点。


推荐阅读