为啥这里需要 std::launder ?

Posted

技术标签:

【中文标题】为啥这里需要 std::launder ?【英文标题】:Why is std::launder required here?为什么这里需要 std::launder ? 【发布时间】:2021-10-22 10:26:08 【问题描述】:

我正在阅读 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();
        
    
;

基本上每个元素所在的每个存储桶/区域/内存地址都是一个字符缓冲区。在存在的每个元素位置,都会在 T 类型的缓冲区中完成新的放置。因此,当返回该缓冲区的位置时,可以将 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 呢?

【问题讨论】:

您必须使用new返回的指针来访问创建的对象。不允许投射 bufferstd::launder 可用于从 buffer 获取有效指针。 附带说明,该评论有些误导。需要使用 std::launder 才能使代码有效。在 c++17 之前缺少所需的工具,使得这样的代码无法合法编写。 是的。 char buffer[32]; MyStruct * ptr = new (&amp;buffer[0]) MyStruct; ptr-&gt;~MyStruct(); 很好(假设满足对齐和大小要求)。 Zebrafish> 是的,添加 std::launder 正是为了有一种合法的方式来编写它,而不是依赖未定义的行为并希望编译器会做正确的事情。 这正是我在上面的第二条评论中提到的:该注释具有误导性。在 C++17 之前,没有合法的方式来编写该代码。 【参考方案1】:
    // note: needs std::launder as of C++17
    return *reinterpret_cast<const T*>(&data[pos]);

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

您可以从reinterpret_cast 取消引用指针的条件很少。这不是其中的一个。您正在将指向一种类型的指针转​​换为指向完全不相关类型的指针,并通读它。见here。

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

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

我已经对此提出了很多问题,并且似乎同意...这显然不是严格的混叠违规,完全没问题。那么为什么这里需要 std::launder 呢?

这不好,出于同样的原因。也没有同意这很好。请参阅@eerorika 在your question here 上的回答。

【讨论】:

如果在动态分配的内存中访问和写入对象的唯一方法是使用placement new,那么这意味着任何将malloc用于int缓冲区而不使用placement new的代码都是不明确的。而且在 C++ 放置 new 之前,不能使用 malloc 作为缓冲区或 int 或任何其他类型吗? @Zebrafish 新位置从 C++ 的第一个完整版本就出现了。在此之前不是这样,但那时您也必须在构造函数中手动分配 this 另外,implicit-lifetime types 有一个special rule,它允许编译器隐式启动满足特定条件的对象的生命周期。标量类型满足这些条件,因此简单地将 malloc 的结果转换为标量数组是明确定义的。不过,只有第一次,不要存储 void* 并再次施放。 @spectras “隐式生命周期类型”。从何时起?您是在谈论新提案吗? open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r5.html ?您是否在该链接中看到,大约一两页,它 malloc 并分配给 struct X 指针,然后访问其成员。它说虽然这是惯用的,但它在 C++ 中是未定义的。但是,从 cppreference 页面来看,struct X 应该是“聚合类型”,即“隐式生命周期”类型。你能澄清一下吗? @Zebrafish 是的,它是最近添加的,可能与 C++20 一样新——我不知道它是否源于这个特定的 PR。在此之前,您需要将placement-new 放入malloc 创建的存储中。

以上是关于为啥这里需要 std::launder ?的主要内容,如果未能解决你的问题,请参考以下文章

C++20 中的 std::launder 用例

我在哪里可以找到 std::launder 的真正作用? [复制]

如何解释 std::launder 的可达性要求?

std::launder 替代 pre c++17

带有就地多态容器的 std::launder

std::launder 的效果是不是在调用它的表达式之后持续?