为什么这里需要使用std::launder?

5
我正在阅读cppreference,在std::aligned_storage的示例中,有一个关于向量/数组类的示例:
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];
    // IF you want to see possible implementation of aligned storage see link.
    // It's very simple and small, it's just a buffer
    
    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{};
 
        // construct value in memory of aligned storage
        // using inplace operator new
        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 
    {
        // note: needs std::launder as of C++17
        // WHY
        return *reinterpret_cast<const T*>(&data[pos]);
    }
 
    // Delete objects from aligned storage
    ~static_vector() 
    {
        for(std::size_t pos = 0; pos < m_size; ++pos) {
            // note: needs std::launder as of C++17
            // WHY?
            reinterpret_cast<T*>(&data[pos])->~T();
        }
    }
};

基本上,每个元素所在的内存桶/区域/地址都是一个char缓冲区。在该缓冲区中存在每个元素位置处,会进行T类型的放置new操作。因此,当返回该缓冲区的位置时,可以将char缓冲区强制转换为T*,因为在那里构造了一个T对象。从C++17开始为什么需要std::launder?我已经问了很多关于这个问题的问题,似乎大家都认为:
alignas(alignof(float)) char buffer [sizeof(float)];
new (buffer) float;// Construct a float at memory, float's lifetime begins
float* pToFloat = buffer; // This is not a strict aliasing violation
*pToFloat = 7; // Float is live here

这显然不是严格别名违规,完全没问题。那么为什么这里需要使用std::launder呢?

1
你必须使用new返回的指针来访问创建的对象。不允许转换buffer。但是,可以使用std::launderbuffer中获取有效指针。 - spectras
2
顺便提一下,这个注释有点误导人。使用std::launder是必需的,以使代码有效。在c++17之前,所需的工具只是缺失了,因此无法合法地编写这样的代码。 - spectras
1
是的。char buffer[32]; MyStruct * ptr = new (&buffer[0]) MyStruct{}; ptr->~MyStruct(); 是可以的(假设对齐和大小要求已满足)。 - spectras
1
斑马鱼 > 是的,std::launder 的添加正是为了有一种合法的方式来编写这个,而不是依赖于未定义的行为并希望编译器做正确的事情。 - spectras
1
这正是我在上面的第二条评论中提到的:那个注释是误导性的。在C++17之前,没有合法的方法来编写那段代码。 - spectras
显示剩余23条评论
1个回答

1
    // note: needs std::launder as of C++17
    return *reinterpret_cast<const T*>(&data[pos]);

为什么从C++17开始需要std::launder?

通过reinterpret_cast解引用指针的条件是非常有限的,而这不是其中之一。您正在将一个指针从一种类型转换为一个完全不相关的类型,并通过它进行读取。请参见此处

std::launder允许您派生出有效的指针。它的存在的全部原因就是给你这个。

在C++17之前,您必须保存从放置new返回的指针,因为那是在视野中唯一有效的指向T的指针,在std::launder之前,您无法从缓冲区创建一个指针。

我已经问了很多关于这个问题的问题,似乎大家都同意...这显然不是一个严格的别名违规,完全没问题。那么为什么这里需要std::launder呢?

那样做并不好,出于同样的原因。也没有达成一致认为这是好的。请参见@eerorika在你的问题链接上的回答。

如果真的只有使用定位new才能访问和写入动态分配内存中的对象,那么这意味着任何使用malloc为int缓冲区而没有使用定位new的代码都是未定义的。同时也意味着,在C++拥有定位new之前,不能使用malloc作为缓冲区来存储int或其他任何类型的数据? - Zebrafish
@Zebrafish placement new 在 C++ 的第一个完整版本中就已经存在了。在那之前是没有的,但那时你还必须手动分配 this 到你的构造函数中。 - spectras
此外,对于隐式生命周期类型,还有一种特殊规则,允许编译器隐式启动满足特定条件的对象的生命周期。标量类型满足这些条件,因此仅仅将malloc的结果强制转换为标量数组是被定义良好的。但只有第一次这样做,不要存储void*并再次进行强制转换。 - spectras
@spectras的“隐式生命周期类型”。从什么时候开始?你是在谈论新提案吗?http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r5.html?你能看到链接中大约一页或两页的内容,它malloc并分配给一个struct X指针,然后访问其成员。它说虽然这是惯用法,但在C++中未定义。但是从cppreference页面struct X应该是一个“聚合类型”,这是一个“隐式生命周期”类型。你能澄清一下吗? - Zebrafish
@Zebrafish 是的,这是最近添加的功能,可能就在 C++20 中 - 我没有关注它是否源自此特定的 PR。在此之前,您需要将 placement-new 放入 malloc 创建的存储中。 - spectras
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接