【问题标题】:aligned_storage and strict aliasing对齐存储和严格别名
【发布时间】:2012-11-08 03:04:03
【问题描述】:

我目前正在使用aligned_storage 来实现类似于boost::optional 的“可选”类型。为了做到这一点,我有一个像这样的班级成员:

typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type t_;

我使用placement new 来创建对象,但是我没有将返回的指针存储在任何地方。相反,我在我的所有成员函数中访问对象的基础类型,就像这样(显然通过检查以确保对象有效,通过也存储在我的 Optional 类型中的布尔标志):

T const* operator->() const {
    return static_cast<T const*>(static_cast<void const*>(&t_));
}

我的问题是这是否安全。我的理解是,我对放置 new 的使用改变了对象的“动态类型”,只要我继续使用该类型访问内存就可以了。但是,我不清楚我是否必须保留从新位置返回的指针,或者是否允许我在需要访问它时只转换为基础类型。我已经阅读了 C++11 标准的第 3.10 节,但是我的标准语不够流利,无法确定。

如果可能的话,如果你能在你的回答中参考标准,我会感觉更好(它有助于我晚上睡觉:P)。

【问题讨论】:

  • 我认为这归结为 operator new 返回的指针是否可能与结果对象的地址不同。 C++03 标准的第 5.3.4.14 节指出,如果对象是数组,则不一定相同,这使我相信否则它会相同。
  • 最终,它的实现定义了分配的基地址是否是对象占用空间的第一个字节。因此,取new 的结果更正确。明显的情况是通过new[] 分配;实现通常将销毁数组对象所需的信息存储在分配的前导字节中。
  • 注意 C++17 中有 std::optional...

标签: c++ undefined-behavior language-lawyer strict-aliasing type-punning


【解决方案1】:

ABICT 您的使用是安全的。

  • T 类型对象的新位置将创建一个从传入地址开始的对象。

§5.3.4/10 说:

new-expression 将请求的空间量传递给 分配函数作为 std::size_t 类型的第一个参数。那 参数不得小于被创建对象的大小; 它可能大于正在创建的对象的大小,仅当 对象是一个数组。

对于非数组对象,分配的大小不能大于对象的大小,因此对象表示必须从分配的内存的开头开始才能适合。

Placement new 返回传入的指针(参见第 18.6.1.3/2 节)作为“分配”的结果,因此构造对象的对象表示将从该地址开始。

  • static_cast&lt;&gt;T* 类型和 void* 之间的隐式转换在指向对象的指针和指向其存储的指针之间转换,如果对象是完整的对象。

§4.10/2 说:

“指向 cv T 的指针”类型的纯右值,其中 T 是对象类型,可以是 转换为“指向 cv void 的指针”类型的纯右值。的结果 将“指向 cv T 的指针”转换为“指向 cv void 的指针”指向 类型 T 的对象所在的存储位置的开始,如 如果对象是 T 类型的最衍生对象 (1.8) [...]

这将隐式转换定义为按说明进行转换。进一步的 §5.2.9[expr.static.cast]/4 为显式转换定义了static_cast&lt;&gt;,其中存在隐式转换与隐式转换具有相同的效果:

否则,表达式e 可以显式转换为类型T 如果声明,则使用 static_cast&lt;T&gt;(e) 形式的 static_cast T t(e); 格式正确,对于某些发明的临时变量 t (8.5)。 这种显式转换的效果与执行 声明和初始化,然后使用临时 作为转换结果的变量。 [...]

对于逆 static_cast&lt;&gt;(从 void*T*),§5.2.9/13 状态:

“指向 cv1 void 的指针”类型的纯右值可以转换为纯右值 类型为“指向 cv2 T”的指针,其中 T 是对象类型,而 cv2 是 与 cv1 相同的 cv 限定或更高的 cv 限定。 [...] 指向对象的类型指针值转换为“指向 cv void 的指针” 并返回,可能具有不同的 cv 资格,应具有其 原值。

因此,如果您有一个 void* 指向 T 对象的存储(这是由 T* 到对象的隐式转换所产生的指针值,那么该对象的 static_castT* 将产生一个指向该对象的有效指针。

回到你的问题,前面的几点暗示如果你有

typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type t_;
void * pvt_ = &t_;

T* pT = new (&t_) T(args...);
void * pvT = pT;

然后

  • *pT 的存储正好覆盖t_ 的存储的第一个 size(T) 字节,因此pvT == pvt_
  • pvt_ == static_cast&lt;void*&gt;(&amp;t_)
  • static_cast&lt;T*&gt;(pvT) == pT
  • 综合起来产生static_cast&lt;T*&gt;(static_cast&lt;void*&gt;(&amp;t_)) == pT

【讨论】:

  • “ABICT”代表什么?
  • 如果一个对象通过放置 new 接收到的指针访问,我认为编译器没有理由关心对象的声明类型,因为使用该类型的访问不能别名使用新类型进行的访问。如果一个对象被“直接”写为左值,并且它的地址稍后通过放置新转换为另一种类型,则要求编译器执行对原始值的写入,而不是重新排序过去的写入,因为新类型在某些情况下可能会阻碍优化.我认为在这种情况下保证行为的好处将超过......
  • ...优化的轻微潜在障碍,但我不确定编译器编写者是否会同意这种判断。然而,对齐指令的大部分价值来自于存储静态或自动持续时间的能力,这些持续时间可用于保存已知最大大小但任意类型的东西。
  • “据我所知”,ABICT?
猜你喜欢
  • 2012-12-19
  • 1970-01-01
  • 2013-03-27
  • 2014-06-28
  • 2017-01-21
  • 1970-01-01
  • 2021-10-21
  • 2019-02-09
  • 2017-10-23
相关资源
最近更新 更多