【问题标题】:Does reinterpret_casting std::aligned_storage* to T* without std::launder violate strict-aliasing rules? [duplicate]reinterpret_casting std::aligned_storage* to T* without std::launder 是否违反严格混叠规则? [复制]
【发布时间】:2018-05-23 22:57:39
【问题描述】:

以下示例来自cppreference.com的std::aligned_storage page:

#include <iostream>
#include <type_traits>
#include <string>

template<class T, std::size_t N>
class static_vector
{
    // properly aligned uninitialized storage for N T's
    typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
    std::size_t m_size = 0;

public:
    // Create an object in aligned storage
    template<typename ...Args> void emplace_back(Args&&... args) 
    {
        if( m_size >= N ) // possible error handling
            throw std::bad_alloc{};
        new(data+m_size) T(std::forward<Args>(args)...);
        ++m_size;
    }

    // Access an object in aligned storage
    const T& operator[](std::size_t pos) const 
    {
        return *reinterpret_cast<const T*>(data+pos);
    }

    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            reinterpret_cast<T*>(data+pos)->~T();
        }
    }
};

int main()
{
    static_vector<std::string, 10> v1;
    v1.emplace_back(5, '*');
    v1.emplace_back(10, '*');
    std::cout << v1[0] << '\n' << v1[1] << '\n';
}

在示例中,operator[] 只是 reinterpret_casts std::aligned_storage* 到 T* 没有 std:launder,并直接执行间接。然而,根据this question,这似乎是未定义的,即使T 类型的对象曾经被创建过。

所以我的问题是:示例程序真的违反了严格的别名规则吗?如果不是,那我的理解有什么问题?

【问题讨论】:

  • 你的问题基本上可以归结为,“这个答案真的说明了什么吗?”是的,它确实说明了它所说的内容。
  • @NicolBolas,因为cppreference.com是一个著名的网站,所以当我的理解与它发生冲突时,我认为我的理解首先有问题。

标签: c++ language-lawyer reinterpret-cast strict-aliasing


【解决方案1】:

我在 ISO C++ 标准 - 讨论论坛中询问了 a related question。我从这些讨论中学到了答案,并在这里写下来,希望能帮助对这个问题感到困惑的其他人。我会根据这些讨论不断更新这个答案。

P0137之前,参考[basic.compound]第3段:

如果一个类型为 T 的对象位于地址 A 处,则称其值为地址 A 的类型为 cv T* 的指针指向该对象,而不管该值是如何获得的。

和 [expr.static.cast] 第 13 段:

如果原始指针值表示内存中某个字节的地址A,并且A满足T的对齐要求,则得到的指针值表示与原始指针值相同的地址,即A。

表达式reinterpret_cast&lt;const T*&gt;(data+pos) 表示先前创建的T 类型对象的地址,因此指向该对象。间接通过这个指针确实得到了那个对象,这是定义明确的。

但是在 P0137 之后,指针值的定义发生了变化,第一个块引用的字被删除。现在参考 [basic.compound] 第 3 段:

指针类型的每个值都是以下之一:

  • 指向对象或函数的指针(指针被称为指向对象或函数),或

  • ...

和 [expr.static.cast] 第 13 段:

如果原始指针值表示内存中一个字节的地址A,并且A不满足T的对齐要求,则结果指针值未指定。否则,如果原始指针值指向对象 a,并且存在与 a 指针可互转换的类型为 T(忽略 cv 限定)的对象 b,则结果是指向 b 的指针。 否则,指针值不会因转换而改变。

表达式reinterpret_cast&lt;const T*&gt;(data+pos) 仍然指向std::aligned_storage&lt;...&gt;::type 类型的对象,并且间接获取指向该对象的左值,尽管左值的类型是const T。示例中表达式v1[0] 的求值尝试通过左值访问std::aligned_storage&lt;...&gt;::type 对象的值,根据[basic.lval] 第11 段(即严格别名规则),这是未定义的行为:

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,

  • 对象动态类型的 cv 限定版本,

  • 与对象的动态类型类似(定义在 [conv.qual] 中)的类型,

  • 对象的动态类型对应的有符号或无符号类型,

  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,

  • 在其元素或非静态数据成员(递归地包括子聚合或包含联合的元素或非静态数据成员)中包含上述类型之一的聚合或联合类型,

  • 对象的动态类型的(可能是 cv 限定的)基类类型,

  • char、unsigned char 或 std​::​byte 类型。

【讨论】:

  • 我想知道std::aligned_storage&lt;T&gt;::type 类型的对象是否仍然存在。我相信给定这段代码std::aligned_storage_t&lt;T&gt; storage; new (static_cast&lt;void*&gt;(&amp;storage)) T{};,在调用placement new之前隐式调用storage的琐碎析构函数,这意味着storage的生命周期已经结束,而&amp;storage唯一存在的对象是键入 T。但这可能不正确。
  • 查看 C++17 草案,第 6.6.3 节指定当该对象占用的存储空间被释放或被另一个对象重用时,具有普通析构函数的对象的生命周期结束。所以我不认为在这种情况下违反了严格的别名,因为std::aligned_storage_t&lt;T&gt; 类型的对象的生命周期在 reinterpret_cast 时已经结束。
  • @SJL 在这种情况下,我没有找到指向生命周期结束对象的指针的任何特殊规则。如果我的答案中的规则仍然适用于生命周期已结束的对象,那么它当然是未定义的,否则我认为它是未定义的,没有提及。
  • 我认为你没有定义行为是对的,但这似乎是一个疏忽。 6.6.2.3 节定义了“提供存储”的概念(我们可以假设std::aligned_storage_t&lt;T&gt; 将满足T 类型)。 6.6.2.4.2 然后将a 定义为嵌套在b 中,如果b“为a 提供存储”。最后,6.6.2.8 指出如果一个对象嵌套在另一个对象中,则允许不同的对象具有相同的地址。看起来确实是为了确保这样的演员阵容是明确定义的,但它没有明确(在 6.6.2 或其他地方)
【解决方案2】:

代码不会以任何方式违反严格的别名规则。 const T 类型的左值用于访问 T 类型的对象,这是允许的。

链接问题涵盖的相关规则是终身规则; C++14 (N4140) [basic.life]/7。问题是,根据这个规则,指针data+pos 可能不能用于操作placement-new 创建的对象。您应该使用placement-new“返回”的值。

问题自然随之而来:指针reinterpret_cast&lt;T *&gt;(data+pos) 呢?目前尚不清楚通过这个新指针访问新对象是否违反 [basic.life]/7。

您链接到的答案的作者假设(没有提供任何理由)这个新指针仍然是“指向原始对象的指针”。但是在我看来,也有可能争辩说,作为T *,它不能指向原始对象,即std::aligned_storage 而不是T。

这表明对象模型未指定。并入 C++17 的提案P0137 正在解决对象模型不同部分的问题。但它引入了std::launder,这是一种mjolnir,可以解决各种别名、生命周期和出处问题。

毫无疑问,std::launder 的版本在 C++17 中是正确的。不过,就我所见,P0137和C++17对于不带launder的版本是否正确,已经没有什么好说的了。

恕我直言,在没有std::launder 的 C++14 中调用代码 UB 是不切实际的,因为除了浪费内存存储放置新的所有结果指针之外,没有其他办法解决这个问题。如果这是 UB,那么在 C++14 中实现 std::vector 是不可能的,这远非理想。

【讨论】:

  • 在 C++14 中,我认为这是很好定义的,因为在这个例子中,新对象是在 指针转换之前创建的,所以转换结果指向[basic.compound]/9 新创建的对象。但是,P0137 更改了指针值的字,这导致转换结果指向原始对象。 std::launder is intended to 处理在指针转换之后创建了一个新对象的情况,但是在P0137更改之后,它使得“之前”的情况未定义没有std::launder。我说的对吗?
  • new words 代表 static_cast 和 those 代表指针值可能是强制转换结果指向原始对象的理由。另外,cpprefernce.com的reinterpret_castpage的“备注”部分的例子也是一个佐证。
  • 另外,恕我直言,在 C++14 中,虽然data+pos 可能不会自动指向新对象,但它的指针值仍然可以在一些有限的方式中使用(例如,强制转换到const T*)。由于reinterpret_cast&lt;const T*&gt;(data+pos)会指向新创建的对象,所以没有问题(*reinterpret_cast&lt;const T*&gt;(data+pos)是用来操作对象的,而不是data+pos)。
猜你喜欢
  • 2018-12-14
  • 2011-04-15
  • 1970-01-01
  • 1970-01-01
  • 2015-10-04
  • 2020-04-11
  • 2023-02-02
  • 2016-02-24
相关资源
最近更新 更多