std::start_lifetime_as() 的实现

max*_*zig 7 c++ lifetime strict-aliasing undefined-behavior c++23

在 C++20 中接受P0593R6(“为低级对象操作隐式创建对象”)后,C++23 将得到std::start_lifetime_as()“完成 [P0593R6] 中提出的功能”(参见P2590R2P2679R2cppreference C++ 23 功能测试页)。

参考实现是什么样子std::start_lifetime_as()的?

这样的事情就足够了,还是还有更多?

#include <cstddef>
#include <new>

template<class T>
    T* start_lifetime_as(void* p) noexcept
{
    new (p) std::byte[sizeof(T)];
    return static_cast<T*>(p);
}
Run Code Online (Sandbox Code Playgroud)

Jan*_*tke 8

std::start_lifetime_as不能完全手工实现,因为它具有不访问存储的特殊属性。我们自己提供的任何实现理论上都必须访问存储,即使这可以在实践中进行优化。

\n

然而,忽略这个细节,我们可以按如下方式实现:

\n

C++20 - 实现方面std::memmove

\n

从 C++20 开始,std::memmovestd::memcpy是“神奇的”,因为它们隐式地在目标处开始对象的生命周期。\nstd::memmove可以具有相同的源和目标,因此我们可以劫持它的神奇属性并std::start_lifetime_as轻松实现:

\n
template<class T>\nrequires (std::is_trivially_copyable_v<T> && std::is_implicit_lifetime_v<T>)\nT* start_lifetime_as(void* p) noexcept\n{\n    return std::launder(static_cast<T*>(std::memmove(p, p, sizeof(T))));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这样做的原因是:

\n
\n

函数 [std::memcpystd::memmove] 在将字符序列复制到目标之前立即在目标存储区域中隐式创建对象。

\n
\n

- [cstring.syn] p3

\n

std::memmove将内存区域转变[p, p + sizeof(T))为隐式创建对象的区域。您可能会问该对象的类型是什么:

\n
\n

对于指定为隐式创建对象的每个操作,该操作会在其指定的存储区域中隐式创建并启动零个或多个隐式生命周期类型的对象的生命周期(如果这样做会导致程序具有已定义的行为)。

\n
\n

- [介绍.对象] p10

\n

如果没有std::launder,则可能会T在此内存区域中创建除 以外类型的对象。但是,std::launder前提条件是必须有一个Tat p(参见[ptr.launder] p2),因此编译器必须创建T那里以满足前一段。

\n

C++17 - 在放置新方面的实现

\n

std::start_lifetime_asstd::memmove即使没有“魔法”也是可以实现的,并且P0593R6准确地解释了如何做到这一点。\n该论文中的解释早于std::memmove给出魔法属性,这就是为什么它建议更复杂的实现:

\n
\n

如果目标类型是可简单复制的隐式生命周期类型,则可以通过以下方式来完成:将存储复制到其他位置,使用类字节类型数组的放置新,并将存储复制回其原始位置,然后使用std::launder获取指向新创建的对象的指针。

\n
\n

- \xc2\xa73.8 直接对象创建

\n
template<class T>\nrequires (std::is_trivially_copyable_v<T> && std::is_implicit_lifetime_v<T>)\nT* start_lifetime_as(void* p) noexcept\n{\n    // 1. Copy the storage elsewhere.\n    std::byte backup[sizeof(T)];\n    std::memcpy(backup, p, sizeof(T));\n    \n    // 2. Use placement new of an array of byte-like type\n    //    according to [intro.objec] p13, this implicitly begins the lifetime\n    //    of an object within the byte storage.\n    //    However, it also turns the memory at p indeterminate.\n    new (p) std::byte[sizeof(T)];\n\n    // 3. Copy the storage back to its original location.\n    //    This turns the object representation determinate again,\n    //    while keeping the implicit object creation at p.\n    std::memcpy(p, backup, sizeof(T));\n\n    // 4. Return a laundered pointer.\n    //    Because T being at the address that p represents is a\n    //    precondition of std::launder, this forces the implicit\n    //    object created via placement new to be of type T.\n    return std::launder(static_cast<T*>(p));\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

注意 1:您的实现存在这样的问题:当您对 a 进行放置 new 时std::byte[],内存会变得不确定,因此不会保留对象表示。

\n

注 2:C++17 版本并不完整,因为它不允许启动volatile类型的生命周期。

\n


Dav*_*ing 5

这些函数纯粹是编译器的魔法:没有办法在 C++ 中实现这些效果(严格解释),这就是它们被添加到库中的原因。

实现经常“以另一种方式看待”,或者无法证明未定义的行为正在发生,但他们抓住任何给定的示例并“错误编译”它只是时间问题。

  • @JanSchultke:这个建议是说明性的,但并不等同:`std::start_lifetime_as`根本不访问存储,这在某些情况下可以防止数据竞争。 (2认同)