为什么std::aligned_storage在C++23中将被弃用,应该使用什么代替?

59
我刚看到C++23计划弃用std::aligned_storagestd::aligned_storage_tstd::aligned_unionstd::aligned_union_t

据我所知,使用对齐存储的放置new对象在constexpr方面不是特别友好,但这似乎不足以抛弃这个类型。这让我假设,我还不知道使用std::aligned_storage和相关函数存在其他基本问题。那会是什么问题?

是否有提议的这些类型的替代品?


2
相关提案似乎是 P1413R3 - 废弃 std::aligned_storage 和 std::aligned_union,这确实已于 2022 年 2 月 7 日 获得批准 - Brian61354270
标准中有一条注释:“可以用带有 alignas(Align) 声明的 std::byte[Len] 数组替换 aligned_storage<Len, Align>::type 的用法。” - Passer By
2
我想知道为什么他们认为“使用aligned_*会引发未定义的行为”。我已经提出了一个关于这个问题的问题:为什么使用std::aligned_storage据称会由于未能“提供存储”而导致UB? - HolyBlackCat
1个回答

60
以下是来自P1413R3的三个摘录:
背景

aligned_*对代码库有害,不应使用。在高层次上:

  • 使用aligned_*会导致未定义行为(类型无法提供存储)
  • 保证不正确(标准仅要求类型至少与请求的大小相同,但没有对大小设置上限)
  • API存在许多问题(请参见“关于API”)
  • 由于API错误,几乎所有用法都涉及相同的重复前期工作(请参见“现有用法”)

关于API

std::aligned_*存在许多糟糕的API设计决策。其中一些是共享的,而一些是特定于每个API的。至于共享的问题,有三个主要问题 [为简洁起见,仅包含一个]

  • 需要使用reinterpret_cast来访问值

std::aligned_*实例上没有.data()甚至没有.data。相反,API要求您获取对象的地址,使用reinterpret_cast<T*>(...)调用它,然后最终间接引用结果指针,给您一个T&。这不仅意味着它不能在constexpr中使用,而且在运行时很容易意外触发未定义行为。reinterpret_cast作为使用API的要求是不可接受的。


建议替代方案

aligned_*的最简单替代方案实际上不是库特性。相反,用户应该使用适当对齐的std::byte数组,可能需要调用std::max(std::initializer_list<T>)。它们分别位于<cstddef><algorithm>头文件中(本节末尾有示例)。不幸的是,这种替换并不理想。要访问aligned_*的值,用户必须在地址上调用reinterpret_cast,将字节读取为T实例。使用字节数组作为替换并不能避免这个问题。尽管如此,重要的是要认识到,在已经存在reinterpret_cast的地方继续使用它并不像在以前不存在它的地方新引入它那么糟糕。...

上述接受的提议中关于废弃 aligned_* 的部分,随后列举了许多示例,比如这两个替代建议:
// To replace std::aligned_storage
template <typename T>
class MyContainer {
private:
    //std::aligned_storage_t<sizeof(T), alignof(T)> t_buff;
    alignas(T) std::byte t_buff[sizeof(T)];
};

// To replace std::aligned_union
template <typename... Ts>
class MyContainer {
private:
    //std::aligned_union_t<0, Ts...> t_buff;
    alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
};

5
另外,声称必须使用reinterpret_cast来实现这种惯用法/类型的论点是错误的,因为placement-new会返回一个正确的T*。因此,可以想象并且很有可能出现一个从未需要重新解释任何指针的用例。不管怎样,现在这个东西已经不存在了,人们必须自己动手了。 - bitmask
7
我们真正需要的是一种新的声明正确类型的数组的方式,而不必构造其元素,例如:[[uninitialized_storage]] T t_arr[len];,其中编译器会分配足够大小和对齐的数组,但不会调用任何构造函数。这使开发人员可以使用"placement-new"按需初始化每个T元素。我知道,这只是一厢情愿而已... - Remy Lebeau
3
如果我正确理解了Barry在这个回答中的脚注,C++23应该可以做到这一点,但我还没有密切关注C++23。另一个肯定可行的选择是在联合体中抛出一个空的平凡结构体以成为活动成员,当T不是活动成员时(例如union { struct empty_type {} empty; T entry; } t_arr[len])。然后,std::construct_at合法地更改了活动成员,并且entry仍然可以在constexpr环境中被访问,而不需要使用reinterpret_cast - Human-Compiler
2
天啊,我讨厌C++标准。他们把一切都弄得那么复杂,却忘记了基础。 - Bas
1
@Bas 标准的目的是全面的,针对编译器实施者。这并不简单。这就像你在制造汽车时会遇到的规范和驾驶手册之间的区别一样。 - undefined
显示剩余16条评论

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