我们可以检测 C++17 中的“琐碎可重定位性”吗?

Dav*_*aim 7 c++ memory object type-traits c++17

在未来的 C++ 标准中,我们将拥有“琐碎可重定位性”的概念,这意味着我们可以简单地将字节从一个对象复制到未初始化的内存块,并简单地忽略/清零原始对象的字节。这样,我们就模仿了 C 风格的复制/移动对象的方式。

在未来的标准中,我们可能会有类似std::is_trivially_relocatable<type>类型特征的东西。目前,我们拥有的最接近的东西std::is_pod<type>将在 C++20 中弃用。

我的问题是,我们在当前标准(C++17)中是否有办法确定对象是否可以轻松重定位?例如,std::unique_ptr<type>可以通过将其字节复制到新的内存地址并将原始字节清零来移动,std::is_pod_v<std::unique_ptr<int>>但是false.

此外,当前的标准要求每个未初始化的内存块都必须通过构造函数才能被视为有效的 C++ 对象。即使我们能以某种方式弄清楚该对象是否可以轻松重定位,如果我们只是移动字节 - 根据标准它仍然是 UB。那么另一个问题是——即使我们可以检测到trivial relocatability,我们怎样才能在不引起UB的情况下实现trivial relocation呢?简单地调用memcpy + memset(src,0,...)并将内存地址转换为正确的类型就是 UB。`

谢谢!

Quu*_*one 8

P1144的作者在这里;不知怎的,我现在才看到这个问题!

\n

std::is_trivially_relocatable<T>是针对 C++ 的某些未来版本提出的,但我不预测它会很快出现(绝对不是 C++23,我打赌不是 C++26,很可能永远不会)。这篇论文(P1144R6,2022年 6 月)应该可以回答您的很多问题,尤其是那些人们正确回答的问题:如果您已经可以在当今的 C++ 中实现这一点,我们就不需要提案了。另请参阅我的 2019 C++Now 演讲

\n

Michael Kenzel 的回答之前说过,P1144“最终要求用户手动标记可以进行[简单重定位]的类型”;我想指出的是,这与我的观点恰恰相反。微不足道的可重定位性的最新技术是对每种类型进行手动标记(“保证”);例如,在《愚蠢》中,你会说

\n
struct Widget {\n    std::string s;\n    std::vector<int> v;\n};\nFOLLY_ASSUME_FBVECTOR_COMPATIBLE(Widget);\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个问题,因为普通的行业程序员不应该费心去弄清楚std::string在他们选择的库上是否可以轻松地重定位。(上面的注释在 3 大供应商的 1.5 上是错误的!)即使Folly 自己的维护人员也无法 100% 正确地获得这些手动注释。

\n

所以 P1144 的想法是编译器可以为你处理它。你的工作从危险地保证你不一定知道的事情变成仅仅(并且可选地)通过(Godbolt验证你想要的事情是真实的:static_assert

\n
struct Widget {\n    std::string s;\n    std::vector<int> v;\n};\nstatic_assert(std::is_trivially_relocatable_v<Widget>);\n\nstruct Gadget {\n    std::string s;\n    std::list<int> v;\n};\nstatic_assert(!std::is_trivially_relocatable_v<Gadget>);\n
Run Code Online (Sandbox Code Playgroud)\n

在您的(OP)特定用例中,听起来您需要找出给定的 lambda 类型是否可以轻松重定位(Godbolt):

\n
void f(std::list<int> v) {\n    auto widget = [&]() { return v; };\n    auto gadget = [=]() { return v; };\n    static_assert(std::is_trivially_relocatable_v<decltype(widget)>);\n    static_assert(!std::is_trivially_relocatable_v<decltype(gadget)>);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是 Folly/BSL/EASTL根本无法做到的事情,因为它们的保证机制仅适用于全局范围内的命名类型。完全不能FOLLY_ASSUME_FBVECTOR_COMPATIBLE(decltype(widget))

\n

std::function类似类型中,您是正确的,知道捕获的类型是否可以轻松重定位是有用的。但由于您无法知道这一点,因此下一个最好的事情(以及您在实践中应该做的事情)就是检查std::is_trivially_copyable. 这是当前受祝福的类型特征,字面意思是“这种类型是安全的memcpy,可以安全地跳过 \xe2\x80\x94 的析构函数”,基本上你将用它做的所有事情。即使您知道该类型正是std::unique_ptr<int>,或者其他什么,在当今的 C++ 中memcpy 它仍然是未定义的行为,因为当前的标准您不允许 memcpy 不可简单复制的类型。

\n

(顺便说一句,从技术上讲,P1144 并没有改变这个事实。P1144 只是说允许实现消除重定位的影响,这对实现者来说是一个巨大的眨眼和点头,他们应该只使用 memcpy。但即使是 P1144R6对于普通的非实现程序员来说,memcpy 非平凡可复制类型是不合法的:它为某些编译器的实现和某些库的实现打开了大门,该函数__builtin_trivial_relocate在某种神奇的意义上可以与一个普通的旧 memcpy。)

\n

最后,你的最后一段提到了memcpy + memset(src,0,...)。那是错误的。琐碎的搬迁无异于只是 memcpy。如果您关心 \xe2\x80\x94 之后源对象的状态,如果您关心它是全零字节,例如 \xe2\x80\x94 那么这一定意味着您要查看再次查看它,这意味着您实际上并未将其视为已损坏,这意味着您实际上并未在此处执行重新定位的语义“复制并清空源”通常是移动的语义。搬迁的目的是避免额外的工作。

\n


Mic*_*zel 5

简单可重定位性的要点是即使存在非简单移动构造函数或移动赋值运算符,也可以按字节移动对象。

\n

编辑:删除了有关 P1144 的错误陈述。

\n

当然,您可以定义自己的is_trivially_relocatable特征,该特征默认为std::is_trivially_copyable_v并让用户专门针对应特别视为可简单重定位的类型。然而,即使这样也是有问题的,因为没有办法自动将此属性传播到由普通可重定位类型\xe2\x80\xa6 组成的类型

\n

即使对于可简单复制的类型,您也不能仅将对象表示的字节复制到某个随机内存位置并将地址转换为指向原始对象类型的指针。由于从未创建对象,因此该指针不会指向对象。尝试访问指针未指向的对象将导致未定义的行为。简单可复制性意味着您可以将对象表示的字节从一个现有对象复制到另一个现有对象,并依赖于使一个对象的值等于另一个[basic.types]/3的值。

\n

要简单地重新定位某个对象,意味着您必须首先在目标位置构造一个给定类型的对象,然后将原始对象的字节复制到该对象中,然后以相当于以下方式修改原始对象:如果你离开那个物体会发生什么。这本质上是移动对象\xe2\x80\xa6的一种复杂方法

\n

将平凡可重定位性概念添加到语言中的提议存在是有原因的:因为您目前无法从语言本身内部做到这一点\xe2\x80\xa6

\n

请注意,尽管如此,编译器前端无法避免生成构造函数调用并不意味着优化器无法消除不必要的加载和存储。让我们看一下编译器为移动 or 的示例生成的std::vector代码std::unique_ptr

\n
auto test1(void* dest, std::vector<int>& src)\n{\n    return new (dest) std::vector<int>(std::move(src));\n}\n\nauto test2(void* dest, std::unique_ptr<int>& src)\n{\n    return new (dest) std::unique_ptr<int>(std::move(src));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,仅仅进行实际的移动通常就已经归结为只是复制和覆盖一些字节,即使对于非平凡的类型\xe2\x80\xa6

\n