reinterpret_casting std::aligned_storage* to T* 没有 std::launder 是否违反严格的别名规则?

Does reinterpret_casting std::aligned_storage* to T* without std::launder violate strict-aliasing rules?

以下例子来自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 的对象。

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

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

linked 问题所涵盖的相关规则是终身规则; C++14 (N4140) [basic.life]/7。问题是,根据这个规则,指针data+pos可能不能用来操作placement-new创建的对象。您应该通过 placement-new.

使用值 "returned"

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

您 link 的答案的作者假设(没有提供任何理由)这个新指针仍然是 "a pointer that pointed to the original object"。然而,在我看来,也有可能争辩说,作为 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,这远非理想。

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

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

If an object of type T is located at an address A, a pointer of type cv T* whose value is the address A is said to point to that object, regardless of how the value was obtained.

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

If the original pointer value represents the address A of a byte in memory and A satisfies the alignment requirement of T, then the resulting pointer value represents the same address as the original pointer value, that is, A.

表达式reinterpret_cast<const T*>(data+pos)表示先前创建的T类型对象的地址,因此指向该对象。通过this指针间接获取到那个对象,定义明确

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

Every value of pointer type is one of the following:

  • a pointer to an object or function (the pointer is said to point to the object or function), or

  • ...

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

If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

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

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • the dynamic type of the object,

  • a cv-qualified version of the dynamic type of the object,

  • a type similar (as defined in [conv.qual]) to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,

  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,

  • an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),

  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,

  • a char, unsigned char, or std​::​byte type.