【问题标题】:What does C++ standard say about moving live object storage?C++ 标准对移动实时对象存储有什么看法?
【发布时间】:2018-08-24 04:05:40
【问题描述】:

我很好奇在当前 C++ 标准下如何解释以下情况,尤其是在生命周期等方面。它是未定义的行为吗?

首先,让我们从以下定义开始:可重定位对象是一个在其实际内存位置上不变的对象——也就是说,无论指针 this 的值如何,它的状态都保持不变。假设我们有一个可重定位类型 Relocatable(它的定义与示例无关)。

那么我们有以下代码(C++17):

typedef std::aligned_storage_t<sizeof(Relocatable)> Storage;

// construct an instance of a relocatable within a storage
auto storage0 = new Storage();
new(storage0) Relocatable(...);

{ 
  // obj is a valid reference
  // should use std::launder() here, but clang doesn't have it yet
  Relocatable& obj = *reinterpret_cast<Relocatable*>(storage0);
}

// move the storage
auto storage1 = new Storage();
memcpy(storage1, storage0, sizeof(Storage));
delete storage0;

{ 
  // ?????? what does the standard say about this?
  Relocatable& obj = *reinterpret_cast<Relocatable*>(storage1);
}

这可以按预期与 GCC 和 Clang 一起使用(对象只是继续存在于新存储中)。但是,我不完全确定标准是否可以。从技术上讲,对象的生命周期还没有结束(没有调用析构函数)并且在 memcpy() 调用之后没有对旧位置中的对象进行任何访问。此外,不存在指向旧位置的引用/指针。尽管如此,鉴于 C++ 似乎在大多数情况下将对象标识和对象存储视为同一件事,因此禁止这样做可能是有原因的。在此先感谢所有有见地的 cmets。

编辑:有人建议Why would the behavior of std::memcpy be undefined for objects that are not TriviallyCopyable? 是这个问题的副本。我不确定是不是。首先,我正在存储存储,而不是对象实例。其次,对于所有实际相关的应用程序,std::is_trivially_copyable&lt;Relocatable&gt;::value 实际上评估为true

附:我问这个问题实际上有一个很好的实际原因。有时,拥有只能存在于其容器中的对象很有用——它们不可复制且不可移动。例如,我目前正在设计一个具有这样一个属性的优化树数据结构——树节点只能存在于树存储中,它们不能被移出或复制——对它们的所有操作都是通过短期引用执行的。为了防止程序员错误(意外复制/移动),我将删除复制和移动构造函数。这具有相当不幸的后果,即节点不能存储在 std::vector 中。放置新的和明确管理的存储可以用来绕过这个限制——但我当然不想 做一些不符合标准的事情。

【问题讨论】:

  • C++ 中没有可重定位的东西(无论如何现在)。只有微不足道的可复制。析构函数也不是对象生命周期结束的必要条件
  • 一个对象是一个存储区域。是的,该标准将对象地址视为其身份。该对象不会继续存在于新地址,这是不可能的,新地址的任何东西都是新对象。为什么这很有趣或很重要?如果其他节点中有指向它们的指针,则您的树节点不能存在于可调整大小的数组中,因为可调整大小的数组会移动其内容并且指针不会跟踪它们的指针。无论语言或您将移动和复制的概念赋予何种精细语义,这都是正确的,
  • @n.m:他们当然可以住在可调整大小的存储中。很明显我不会使用实际的指针——节点引用存储为压缩偏移量(也提高了约 10% 的性能)。
  • 那么实际使用移动构造函数来移动它们有什么问题呢?没有对 reinterpet_cast 或 memcpy 的 hideous 调用?

标签: c++ language-lawyer move-semantics


【解决方案1】:

因此,与所有这些类型的问题一样,对象仅在four situations 中创建:

对象由定义([basic.def])、new-expression、隐式更改联合的活动成员([class.union])或当创建了一个临时对象([conv.rval], [class.temporary])。

这段代码:

auto storage1 = new Storage();
memcpy(storage1, storage0, sizeof(Storage));

storage1 处为您提供Storage 类型的对象,但此时没有创建Relocatable 类型的对象。因此,这是:

Relocatable& obj = *reinterpret_cast<Relocatable*>(storage1);

是未定义的行为。期间。


为了为此定义行为,我们需要第五种机制来创建对象,例如 P0593 中提出的:

我们建议至少将以下操作指定为隐式创建对象:[...]

  • memmove 的调用就像它一样

    1. 将源存储复制到临时区域

    2. 在目标存储中隐式创建对象,然后

    3. 将临时存储复制到目标存储。

    这允许memmove 保留可平凡复制对象的类型,或用于将一个对象的字节表示重新解释为另一个对象的字节表示。

  • memcpy 的调用与对memmove 的调用行为相同,只是它在源和目标之间引入了重叠限制。

这个提议(或类似的提议)对于您的代码格式正确是必要的。

【讨论】:

  • 太好了,正是我一直在寻找的那种信息!目前,我正在实现两个代码版本,一个使用移动构造函数,另一个假设 realloc 进行初始化(目前所有主要编译器似乎都是这种情况)。当然,有人可能会争辩说这一切都没有实际意义,因为涉及移动/复制构造函数的循环无论如何都针对 memcpy 进行了优化。但是,在某些情况下您可以进行虚拟内存重新映射,这可以在没有任何内存副本的情况下增长容器。我认为 C++ 需要的是“不安全”障碍的集合(std::launder 是第一步)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-20
  • 2015-06-21
  • 2012-10-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多