关于 constexpr 分配有什么难的?

C++20 之前常量求值期间不能进行任何分配,否则求值会失败。C++20 因[P0784R7]改变,可在常量求值期间进行分配,但极其有限,分配必须在该常量求值期间释放。这开启了一系列之前不可能的操作,如可拥有局部std::stringstd::vector<T>,但仍不能声明constexpr std::vector等。

这里实际上有两个问题要解决:

  1. 如何知道可以允许分配持续存在?
  2. 如何知道何时可以将分配的内容用作常量表达式?即常量销毁问题和常量访问问题。

以简化版std::unique_ptr<T>为例说明常量销毁问题:如今constexpr auto p = unique_ptr<int>(new int(42));这类代码格式错误,因为分配在全局变量p的初始化常量求值后仍持续存在,但我们希望改变这一情况。若按期望改变,p在程序结束时会被销毁,其析构函数在运行时运行,而初始化时未在运行时分配内存,不能在运行时释放内存,尝试访问p.ptr可能导致崩溃。所以需要在常量求值时检查p的析构函数是否清理了所有分配,若未清理则初始化失败,若清理则允许其持续存在,但省略运行时p的析构函数调用。

对于多层间接的情况,如constexpr auto ppi = unique_ptr<unique_ptr<int>>(new unique_ptr<int>(new int(42)));,会出现问题,因为内层unique_ptr是可变的,虽外层unique_ptr的析构函数能清理所有分配,但运行时可改变分配,所以检查在编译时析构函数清理分配无意义。可通过使内层unique_ptr为常量来解决此问题,如unique_ptr<unique_ptr<int> const>。对于vector<T>unique_ptr<T>,它们在实际成员方面类似,都存在多层分配问题,若拒绝unique_ptr<unique_<int>>的规则也拒绝vector<vector<int>>,则该规则无用。

对于常量访问问题,若允许持久的constexpr分配,其内容应可作为常量表达式使用,如constexpr auto v = vector<int>{1, 2, 3};应能进行各种常量表达式操作,但实际存在问题,如constexpr auto p = unique_ptr<int>(new int(42));在后续代码中对*p的访问可能导致不一致,代码要么在p声明处格式错误,要么在*p使用处格式错误。

还有一种非持久的constexpr分配情况,如constexpr auto a = unique_ptr<int>(new int(1));可在运行时使用,而consteval auto g() -> int { constexpr auto c = unique_ptr<int>(new int(3)); return *c; }consteval函数中分配的则不可在运行时使用,禁止在常量求值期间对constexpr变量所拥有的分配进行突变可解决一些问题,但会导致相同代码在不同上下文行为不同。

提出的解决方案分为两类:

  1. propconst:通过引入新的cv-限定符propconst来提升const在类型系统中的传播,可用于区分std::vector<T>std::unique_ptr<T>等类型,以解决常量销毁和常量访问问题,但propconst的实现和使用存在一些问题,如在类型系统中的位置、检查的复杂性等。
  2. 信任(Trust):不进行严格验证,而是提供一个魔法库函数std::mark_immutable_if_constexpr(),让库作者标记分配为不可变,若分配已标记为不可变则允许其持续存在且内容可作为常量表达式使用,此方法简单但可能导致不安全的分配,也可直接规定标准库提供的std::basic_stringstd::vector的特化可持久存在且可在常量表达式中使用,但这不够令人满意。

目前处于困境,在圣路易斯会议上进行了一些方向性投票,对于是否允许非持久分配和是否直接支持std::basic_stringstd::vector的持久分配存在不同意见,作者认为自己最喜欢std::mark_immutable_if_constexpr()的设计,虽不提供propconst那样的保护,但简单易懂,且若对const传播类型有疑问,可通过库解决方案解决,无论如何,在 C++26 时间范围内提供对持久std::stringstd::vector的支持很重要。

阅读 16
0 条评论