C++中从std::aligned_storage提前推导T指针是否符合标准安全?
你给出的示例代码在C17下属于未定义行为,必须搭配std::launder才能符合标准要求;C20对部分场景做了放宽,但你的Promise/Future实现框架仍然需要修正才能保证合法性。
C++17标准下的判定依据
std::aligned_storage_t的底层是一个满足指定大小和对齐要求的平凡数组类型(通常是unsigned char/std::byte数组)。你对&storage做reinterpret_cast<T*>得到的指针,初始指向的是这个数组对象,而非后续placement new构造的T对象。
【C++17 [basic.life] §6】
若程序在对象生命周期开始前就使用了指向其已分配存储的指针,仅可进行有限的合法操作,以下操作均属于未定义行为:
- 解引用该指针
- 访问对象的非静态数据成员
- 调用对象的非静态成员函数
当新对象被构造到该存储位置后,只有满足以下所有条件时,原指针才能直接用来访问新对象:
- 新对象与原指针指向的对象是相似类型
- 新对象是该存储位置上构造的第一个该类型对象
- 新对象不含const限定的非静态数据成员或虚函数
显然T和aligned_storage_t的底层数组不属于相似类型,因此原指针无法直接用来访问新构造的T对象,必须通过std::launder获取指向新对象的合法指针。
【C++17 [ptr.launder] §1】
template <class T> constexpr T* launder(T* p) noexcept;
要求:p指向的存储中已存在一个与T类型兼容的对象,且该对象的存储可被返回值合法访问。
返回值:指向p所指存储位置上的T类型对象的有效指针。
因此你给出的第一段示例代码的正确写法应该是:
std::aligned_storage_t<sizeof(T), alignof(T)> storage; new (&storage) T; // 必须在placement new之后调用std::launder T* ptr = std::launder(reinterpret_cast<T*>(&storage)); ptr->DoSomething();
你原来提前在placement new之前就存储转换后指针的做法,即使后续加了launder也不行——因为std::launder必须在对象构造完成后调用,才能获得有效指针。
C++20的规则调整
C++20新增了隐式对象创建规则([intro.object] §10),对于std::aligned_storage_t这类底层是隐式生命周期类型数组的存储,如果你在其上构造的T也是隐式生命周期类型(即平凡默认构造、平凡析构、无虚函数、无const非静态成员),那么可以不需要显式调用std::launder,编译器会自动推导指针的有效性。
但如果你的T不符合隐式生命周期类型的要求,仍然必须显式调用std::launder才能保证合法。此外你提前在对象构造前存储指针的做法,在C++20下仍然属于未定义行为,因为指针不会自动绑定到后续构造的新对象上。
Promise/Future实现的修正方案
首先你给出的示例代码里有笔误:Future构造函数中的reinterpret_cast<U>(promise->storage_)应该是reinterpret_cast<const T*>(&promise->storage_)。
要实现你要的协变能力且保证符合标准、无运行时开销,可以参考以下修正方案:
template <typename T> class Promise { private: std::aligned_storage_t<sizeof(T), alignof(T)> storage_; bool ready_ = false; public: void SetValue(T value) { new (&storage_) T(std::move(value)); ready_ = true; } // 也可以将Future设为友元来直接访问storage_ const void* get_storage() const { return &storage_; } }; template <typename T> class Future { private: const void* storage_; public: // 前置要求:U公开继承自T template <typename U> Future(Promise<U>* promise) : storage_(promise->get_storage()) {} // 前置要求:已调用Promise::SetValue const T& Get() const { // 先launder得到U类型的有效指针,再转为基类T的指针 const U* derived_ptr = std::launder(static_cast<const U*>(storage_)); return *derived_ptr; } };
这个方案的优势是:
std::launder是编译期操作,没有任何运行时开销,完全满足你对高效的要求- 不管是C17还是C20都符合标准
- 完美支持协变场景,只要U是T的公共派生类即可合法转换
内容的提问来源于stack exchange,提问作者jacobsa

