常量表达式

来自cppreference.com
< cpp‎ | language

定义能在编译时求值的表达式

这种表达式能用做非类型模板实参、数组大小,并用于其他要求常量表达式的语境,例如

int n = 1;
std::array<int, n> a1; // 错误:n 不是常量表达式
const int cn = 2;
std::array<int, cn> a2; // OK:cn 是常量表达式

核心常量表达式

核心(core)常量表达式是求值过程中不会对下列任一者求值的表达式:

  1. this 指针,但作为该表达式的一部分求值的 constexpr 函数之中的则不算
  2. (C++23 起) 经过静态或线程存储期变量声明的控制流
  3. 调用未声明为 constexpr 的函数(或构造函数)的函数调用表达式
    constexpr int n = std::numeric_limits<int>::max(); // OK:max() 是 constexpr
    constexpr int m = std::time(NULL); // 错误:std::time() 非 constexpr
  4. 对声明为 constexpr 但未定义的函数进行的函数调用
  5. constexpr 函数/构造函数模板实例化进行的函数调用,其中该实例化无法满足 constexpr 函数/构造函数的要求。
  6. (C++20 起)constexpr 虚函数的函数调用,调用所用的对象不可用于常量表达式(见后述)且其生命期始于此表达式之外。
  7. 会超出实现定义限制的表达式
  8. 求值导致任何形式的核心语言未定义行为(包含有符号整数溢出、除以零、数组边界外的指针算术等)的表达式。是否检测标准库的未定义行为是未指明的。
    constexpr double d1 = 2.0/1.0; // OK
    constexpr double d2 = 2.0/0.0; // 错误:未定义
    constexpr int n = std::numeric_limits<int>::max() + 1; // 错误:溢出
    int x, y, z[30];
    constexpr auto e1 = &y - &x; // 错误:未定义
    constexpr auto e2 = &z[20] - &z[3]; // OK
    constexpr std::bitset<2> a; 
    constexpr bool b = a[2]; // UB,但是否检测未指定
  9. (C++17 前)lambda 表达式
  10. 左值到右值隐式转换,除非……
    1. 应用到指代可用于常量表达式(见后述)的对象的非 volatile 泛左值,
      int main() {
          const std::size_t tabsize = 50;
          int tab[tabsize]; // OK : tabsize 是常量表达式
                            // 因为 tabsize 可用于常量表达式
                            // 因为它有 const 限定的整数类型,且其初始化器是常量初始化器
       
          std::size_t n = 50;
          const std::size_t sz = n;
          int tab2[sz]; // 错误: sz 不是常量表达式
                        // 因为 sz 不可用于常量表达式
                        // 因为其初始化器不是常量初始化器
      }
      }
    2. 或应用到拥有字面类型,并指代生命期始于此表达式的求值之内的非 volatile 对象
  11. union 的不活跃成员或其子对象实施的左值到右值隐式转换或修改操作(即使它与活跃成员共享公共起始序列也是如此)
  12. (C++20 起) 应用到拥有不确定值的对象的左值到右值隐式转换
  13. 对活跃成员(若存在)为 mutable 的 union 调用隐式定义的复制/移动构造函数或复制/移动赋值运算符,除非该 union 对象的生命期始于此表达式的求值之内
  14. (C++17 起) (C++20 前)会更改 union 的活跃成员的赋值表达式,或对重载的赋值运算符的调用
  15. 指代引用类型的变量或数据成员的 标识表达式,除非该引用可用于常量表达式,或其生命始于此表达式的求值之内
  16. cv void* 转换到任何对象指针类型
  17. (C++20 前)dynamic_cast
  18. reinterpret_cast
  19. (C++20 前)伪析构函数调用
  20. (C++14 前)自增或自减运算符
  21. (C++14 起) 修改对象,除非该对象拥有非 volatile 字面类型,且其生命期始于此表达式的求值之内

    constexpr int incr(int& n) {
      return ++n;
    }
    constexpr int g(int k) {
      constexpr int x = incr(k); // 错误:incr(k) 不是核心常量表达式
                                 // 因为 k 的生命期始于表达式 incr(k) 之外
      return x;
    }
    constexpr int h(int k) {
      int x = incr(k); // OK:不要求 x 以核心常量表达式初始化
      return x;
    }
    constexpr int y = h(1); // OK:以值 2 初始化 y
                            // h(1) 是核心常量表达式
                            // 因为 k 的生命期始于表达式 h(1) 之内
  22. (C++20 起) 对于生存期不在此表达式的求值内开始的对象的析构函数调用或伪析构函数调用
  23. (C++20 前)应用到多态类型泛左值的 typeid 表达式
  24. new 表达式,除非选择的分配函数是全局可替换分配函数且此表达式的求值内解分配其所分配的存储 (C++20 起)
  25. delete 表达式,除非它解分配于此表达式的求值内分配的存储区域 (C++20 起)
  26. (C++20 起)调用 std::allocator<T>::allocate ,除非于此表达式的求值内解分配其所分配的存储
  27. (C++20 起)调用 std::allocator<T>::deallocate ,除非它解分配于此表达式的求值内分配的存储区域
  28. (C++20 起)await 表达式yield 表达式
  29. (C++20 起)结果未指定三路比较
  30. 结果未指明的相等或关系运算符
  31. (C++14 前)赋值或复合赋值运算符
  32. throw 表达式
  33. (C++20 起) 汇编声明
  34. (C++14 起)va_arg 的调用,是否能求值宏 va_start 是未指定的
  35. (C++23 起) goto 语句
  36. (C++20 起)会抛出异常的 dynamic_casttypeid 表达式
  37. lambda 表达式中,提及 this 或提及定义于该 lambda 之外的变量,若它是一次 ODR 式使用
    void g() {
      const int n=0;
      constexpr int j=*&n; // OK:lambda 表达式之外
      [=]{ constexpr int i=n;  // OK:'n' 未被 ODR 式使用且未在此处被俘获。
           constexpr int j=*&n;// 非良构:'&n' ODR 式使用了 'n'。
         };
    }

    注意,若 ODR 式使用发生于对闭包的函数调用中,则它不涉指 this 或外围变量,因为它所访问的是该闭包的数据成员

    // OK:'v' 与 'm' 被 ODR 式使用,但未出现于嵌套于 lambda 内的常量表达式中
    auto monad = [](auto v){return [=]{return v;};};
    auto bind = [](auto m){return [=](auto fvm){return fvm(m());};};
    // 在常量表达式求值中,创建对自动对象的俘获是 OK 的。
    static_assert(bind(monad(2))(monad)() == monad(2)());
    (C++17 起)

注意:核心常量表达式本身并无任何直接的语义含义:表达式必须为常量表达式的子集之一(见后述),才可以在特定语境中使用。

常量表达式

常量表达式(constant expression)

  • 指代下列之一的左值 (C++14 前)泛左值 (C++14 起)核心常量表达式
  • 拥有静态存储期且非临时的对象,或
  • 拥有静态存储期的临时对象,但其值满足下文对纯右值的约束,或
(C++14 起)
  • 其值满足下列约束的纯右值核心常量表达式
  • 若其值是类类型对象,则其每个引用类型的非静态数据成员均指代满足上述对左值 (C++14 前)泛左值 (C++14 起)约束的实体
  • 若其值为指针类型,则它保有
  • 拥有静态存储期的对象的地址
  • 拥有静态存储期的对象的末尾后一位置的地址
  • 立即 (C++20 起)函数的地址
  • 空指针值
  • 若其值是成员函数指针类型,则它不代表立即函数
(C++20 起)
  • 若其值具有类或数组类型,则每个子对象均满足这些对值的约束
void test() {
    static const int a = std::random_device{}();
    constexpr const int& ra = a; // OK:a 是泛左值常量表达式
    constexpr int ia = a; // 错误:a 不是纯右值常量表达式
 
    const int b = 42;
    constexpr const int& rb = b; // 错误:b 不是泛左值常量表达式
    constexpr int ib = b; // OK:b 是纯右值常量表达式
}

整型常量表达式

整型常量表达式是隐式转换成纯右值的整型或无作用域枚举类型的表达式,其中被转换的表达式是核心常量表达式。若将类类型表达式用在期待整型常量表达式之处,则表达式将被按语境隐式转换成整型或无作用域枚举类型。

下列语境要求整型常量表达式

(C++14 前)

经转换的常量表达式

T 类型的经转换的常量表达式隐式转换到 T 类型的表达式,其中被转换后表达式是常量表达式,且隐式转换序列只含有:

  • constexpr 用户定义转换(故类能用于期待整数类型之处)
  • 左值到右值转换
  • 整型提升
  • 非窄化整型转换
  • 数组到指针转换
  • 函数到指针转换
  • 函数指针转换(指向 noexcept 函数的指针到指向函数的指针)
  • 限定性转换
  • 源自 std::nullptr_t 的空指针转换
  • 源自 std::nullptr_t 的空成员指针转换
(C++17 起)
  • 而若发生任何引用绑定,则它是直接绑定(非构造临时对象者)

下列语境要求经转换的常量表达式

(C++14 起)

按语境转换的 bool 类型常量表达式按语境转换到 bool 的表达式,其中转换后的表达式是常量表达式,且转换序列只含上述转换。

下列语境要求按语境转换的 bool 类型常量表达式

(C++23 前)
(C++17 起)
(C++23 前)
(C++20 起)

历史类别

下列常量表达式的类别从 C++14 起不再于标准中使用:

  • 字面常量表达式是非指针字面类型(经语境所要求的转换后)的纯右值核心常量表达式。数组或类类型的字面常量表达式要求每个子对象以常量表达式初始化。
  • 引用常量表达式是指代具有静态存储期的对象或指代函数的左值核心常量表达式。
  • 地址常量表达式std::nullptr_t 类型或指针类型(经语境所要求的转换后)的纯右值核心常量表达式,它指向具有静态存储期的对象,指向具有静态存储期的数组末尾后一位置,指向函数,或为空指针。

可用于常量表达式

上述列表中,变量在 P可用于常量表达式

  • 若该变量是
  • 引用类型,或
  • const 限定的整数或枚举类型。
  • 而该变量的定义从 P 可达
  • 而若 P 不在与该变量定义相同的翻译单元中(即定义是导入的),则该变量未被初始化为指向或指代,或拥有(可能递归的)子对象或成员指向或指代可用于常量表达式的翻译单元局部实体
(C++20 起)

对象或引用可用于常量表达式,若它是

  • 可用于常量表达式的变量,或
  • (C++20 起) 模板形参对象,或
  • 字符串字面量对象,或
  • 以上任一者的非 mutable 子对象或引用成员,或
  • 非 volatile 的 const 限定整数类或枚举类型的,以常量表达式初始化的完整临时对象。
const std::size_t sz = 10; // sz 可用于常量表达式

明显常量求值的表达式

下列表达式(包括到目标类型的转换)是明显常量求值的:

  • 语法上要求常量表达式处,包括
(C++20 起)
(C++17 起)
(C++20 起)
  • 可用于常量表达式的变量的初始化器,包括
  • constexpr 变量的初始化器
  • 引用和 const 限定的整数或枚举类型的变量的初始化器,若该初始化器为常量表达式
  • 静态及线程局域变量的初始化器,若该初始化器的所有子表达式(含构造函数调用和隐式转换)均为常量表达式(即该初始化器为常量初始化器

注意最后两种情况的语境亦接受非常量表达式。

能用 std::is_constant_evaluatedif consteval (C++23 起) 检测求值是否出现于明显常量求值语境中。

为测试最后二类条件,编译器可能及对初始化器的试探性地常量求值。不建议依赖此时的结果。

(C++20 起)

常量求值所需要的函数与变量

下列表达式或转换为潜在常量求值的:

  • 明显常量求值的表达式
  • 潜在求值的表达式
  • 花括号初始化器列表的立即子表达式(可能需要常量求值确定转换是否为窄化
  • 出现于模板化实体内的取址(一元 &)表达式(可能需要常量求值确定这种表达式是否为值待决
  • 上述之一的子表达式,除了嵌套的不求值操作数的子表达式

若函数为 constexpr 函数且为潜在常量求值的表达式所指名,则它为常量求值所需要

若变量为 constexpr 变量或非 volatile 的 const 限定的整数类型或引用类型的变量,且指代它的标识表达式被潜在常量求值,则它为常量求值所需要

预置的函数的定义及函数模板特化变量模板特化 (C++14 起)的实例化会被触发,若该函数或变量 (C++14 起)为常量求值所需要。

注解

不容许实现将库函数声明为 constexpr,除非标准说该函数为 constexpr

常量表达式中不容许具名返回值优化( NRVO ),而返回值优化( RVO )是强制要求的。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1313 C++11 容许未定义行为;且禁止所有指针减法 禁止 UB ;同数组内的指针减法 OK
CWG 1581 C++11 未要求定义或实例化常量求值所需要的函数 已要求
CWG 1952 C++11 要求诊断标准库未定义行为 未指定是否诊断库 UB
CWG 2167 C++11 求值中局部的非成员引用会令求值为非 constexpr 允许非成员引用
CWG 2299 C++14 <cstdarg> 中的宏能否用于常量求值不明确 禁止 va_arg ,未指定 va_start
CWG 2490 C++20 常量求值中的(伪)析构函数调用缺少限制 添加了限制

参阅

constexpr 说明符(C++11) 指定变量或函数的值能在编译时计算
(C++11)(C++17 中弃用)(C++20 中移除)
检查类型是否为字面类型
(类模板)