C++14 中的放置新 + reinterpret_cast:格式良好?

Placement new + reinterpret_cast in C++14: well-formed?

提问人:user1011113 提问时间:10/30/2023 更新时间:11/9/2023 访问量:193

问:

请考虑 C++14 中的以下示例:

alignas(T) unsigned char data[sizeof(T)];
new (data) T();
T* p = reinterpret_cast<T*>(data);
p->something();  // UB?

此代码是否合法,或者是否违反了严格别名规则,因为可能不是 ?如果它是合法的,那么标准的哪些部分明确规定了这一点?unsigned char*T*

cppreference 有一个类似的示例,其声明必须在 C++17 中使用。这对我们没有的 C++14 意味着什么?std::launderstd::launder

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const
    {
        // Note: std::launder is needed after the change of object model in P0137R1
        return *std::launder(reinterpret_cast<const T*>(&data[pos]));
    }
C++ 语言-律师 未定义行为 重新解释-强制转换 严格别名

评论

1赞 HolyBlackCat 10/30/2023
在 C++14 和之前,这是不可能的,但编译器从未强制执行(并且仍然不强制执行,AFAIK),除了少数少数情况,所以没有人关心。
3赞 BoP 10/30/2023
std::launder()即使 C++14 中一切正常,也没有添加。:-)此外,返回指向它所创建内容的有效指针。为什么不直接保存呢?new
1赞 bitmask 10/30/2023
你是对的,你必须清洗重新浸渍的指针。但是,如果您负担得起,可能值得存储放置新位置返回的原始指针,从而避免每次访问都重新洗手,这可能会也可能不会阻止优化机会。
0赞 Quimby 10/30/2023
在内存位置有一个对象,由指向,因此混叠规则与它无关。 由于生存期规则,在洗涤后可以通过返回的指针访问,因此不必“更新”以指向构造对象 - 因此需要优化和修复可达性分析。Tdatastd:launderdataT
0赞 bitmask 10/30/2023
这回答了你的问题吗?std::launder的目的是什么?

答:

-3赞 G. Sliepen 10/30/2023 #1

其他答复和评论已经解决了格式良好的问题。但是,您可以完全避免此问题; 您可以利用 placement- 运算符返回指向 .所以与其写:newT

new (data) T();
T* p = reinterpret_cast<T*>(data);

你可以这样写:

T* p = new (data) T();

在这种情况下,所以完全没有必要,这是 C++14 及更早版本中的有效代码。std::launder()

评论

3赞 user1011113 10/30/2023
不幸的是,在我的用例中,存储从放置 new 返回的指针可能会对内存占用产生不可忽视的影响。用例类似于 static_vector:en.cppreference.com/w/cpp/types/aligned_storage
0赞 Red.Wave 10/30/2023
@user1011113 无论对记忆的影响是什么,当它回来时都会发生。放弃返回值不会优化任何内容。您需要更详细地解释为什么不使用 .如果你认为放置可以分配记忆,你需要重新阅读你的书。newnew
2赞 bitmask 10/30/2023
@Red.Wave 如果你做了一个静态向量的东西,那么你就会有一个大的缓冲区和一堆分配给该缓冲区的小对象,你以后想随机访问它们。存储所有这些指针会以各种方式降低性能。坦率地说,这是一个开放的问题(据我所知)在当前的 C++ 中没有令人满意的解决方案。
2赞 463035818_is_not_an_ai 10/30/2023
OP问的不是“如何做到这一点?”,而是“这个代码合法吗?”。人们可以避免编写 OP 代码并编写其他代码,这并不是回答这个问题。
0赞 Red.Wave 10/30/2023
如果您事先知道布局,那么只要您知道如何进行延迟初始化,指向布局的单个指针就足够了。如果你在运行时之前不知道布局,那么你注定要为每个对象创建一个指针。无论哪种情况,您都需要在 OP 中提供很多东西。
3赞 MSalters 10/30/2023 #2

是的。正如您所发现的,这是您需要 C++17 的情况。对于 C++14 来说,这意味着您需要存储 的返回值,即使这会带来一些开销。std::laundernew

这并不奇怪。引入 C++17 的所有相关更改都是合理的原因之一是您在 C++14 中看到的开销。std::launder

请注意,这种开销在十年前并没有真正实现,因为它主要是错过了优化开销。只有当优化器开始变得更聪明时,它才开始发挥作用。另一方面,像这个问题这样的非法代码在旧的编译器中并不容易被破坏,即使它从来都不是合法的。

评论

0赞 Brian Bi 10/31/2023
我还没有听说过任何编译器在这种情况下执行基于缺失的优化,当他们最终实现它们时,我真的怀疑他们会在 C++14 中应用它们并强制所有 C++14 代码库存储返回值,尽管有开销。我想说的是,在 C++14 模式下,您可以假装指针互换性限制不存在。laundernew
0赞 user1011113 10/31/2023
我觉得令人困惑的部分是 Strict Alising 规则谈论的是“读取或修改对象的存储值”。此外,在调用 placement new 时,会在存储上创建一个新对象。那么问题来了:严格锯齿规则是否适用于这种情况?放置 new 后 char 数组还是一个简单的 char 数组,还是成为新对象 T?
0赞 user1011113 10/31/2023
换句话说,timsong-cpp.github.io/cppwp/n4140/basic.life#2 表示,当原始阵列的存储被重用时,原始阵列的生命周期就结束了。所以在这种情况下,在放置 new 之后,我们真的不再有一个简单的 char 数组,而实际上是一个 T 类型的对象?在这种情况下,我们将通过 T&?
1赞 supercat 11/1/2023
@user1011113:标准可能认为当存储被重用时,生命周期会隐式结束,但这种抽象模型在优化范围之间产生了巨大的鸿沟,优化范围几乎总是可以正确处理所有实际出现的情况,而优化范围可以处理所有可能出现的极端情况,因此实际上与 clang 和 gcc 的实际行为方式不匹配。
0赞 user1011113 11/2/2023
从本质上讲,我想了解代码违反了标准的哪一部分:1)严格的别名规则,2)生存期(指向其生存期结束的重用存储的指针),还是两者兼而有之?你能澄清@MSalters吗?
0赞 user1011113 11/9/2023 #3

感谢大家的回复!我将尝试结合我从回答中获得的知识来回答这个问题。

不存在严格别名冲突

根据 basic.life#2

数组对象的生存期在获得具有适当大小和对齐方式的存储时立即开始,当数组占用的存储被重用或释放时,其生存期结束

因此,在放置调用之后,数组不再包含 类型的对象。它包含一个新创建的对象。newcharcharT

稍后,我们通过 ,这是一个有效的别名来访问该类型的对象。因此,此处不违反严格锯齿规则。TT*

辈子

真正的问题是寿命。当我们 时,我们使用指向过期数据的指针 (),因为它已被新对象替换。不能保证指针已“更新”以指向新创建的对象。reinterpret_cast<T*>(data)datadata

  • 在 C++14 中,合法执行此操作的唯一方法是通过 placement 返回的指针访问对象。Tnew
  • 此外,在 C++17 中,我们可以通过旧指针访问对象,只要我们通过 .这样就不必存储 placement 返回的指针。datastd::laundernew