常量表达式

来自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 函数或 constexpr 构造函数之中的则不算
  2. 调用未声明为 constexpr 的函数(或构造函数)的函数调用表达式
    constexpr int n = std::numeric_limits<int>::max(); // OK:max() 是 constexpr
    constexpr int m = std::time(NULL); // 错误:std::time() 非 constexpr
  3. 对声明为 constexpr 但未定义的函数进行的函数调用
  4. constexpr 函数/构造函数模板实例化进行的函数调用,其中该实例化无法满足 constexpr 函数/构造函数的要求。
  5. (C++20 起)constexpr 虚函数的函数调用,调用所用的对象不可用于常量表达式(见后述)且其生命期始于此表达式之外。
  6. 会超出实现定义限制的表达式
  7. 求值导致任何形式的核心语言未定义行为(包含有符号整数溢出、除以零、数组边界外的指针算术等)的表达式。是否检测标准库的未定义行为是未指明的。
    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,但是否检测未指定
  8. (C++17 前)lambda 表达式
  9. 左值到右值隐式转换,除非……
    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 对象
  10. union 的不活跃成员或其子对象实施的左值到右值隐式转换或修改操作(即使它与活跃成员共享公共起始序列也是如此)
  11. 对活跃成员(若存在)为 mutable 的 union 调用隐式定义的复制/移动构造函数或复制/移动赋值运算符,除非该 union 对象的生命期始于此表达式的求值之内
  12. (C++17 起) (C++20 前)会更改 union 的活跃成员的赋值表达式,或对重载的赋值运算符的调用
  13. 指代引用类型的变量或数据成员的 标识表达式,除非该引用可用于常量表达式,或其生命始于此表达式的求值之内
  14. cv void* 转换到任何对象指针类型
  15. (C++20 前)dynamic_cast
  16. reinterpret_cast
  17. 伪析构函数调用
  18. (C++14 前)自增或自减运算符
  19. (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) 之内
  20. (C++20 前)应用到多态类型泛左值的 typeid 表达式
  21. new 表达式或 delete 表达式
  22. (C++20 起)结果未指定三路比较
  23. 结果未指明的相等或关系运算符
  24. (C++14 前)赋值或复合赋值运算符
  25. throw 表达式
  26. (C++20 起)会抛出异常的 dynamic_casttypeid 表达式
  27. 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 起)

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

可用于常量表达式

上述列表中,变量可用于常量表达式,若它是

  • 引用类型,或
  • const 限定的整数或枚举类型。

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

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

整型常量表达式

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

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

(C++14 前)
  • 位域长度
  • 底层类型未固定时的枚举初始化器
  • 空指针常量
(C++14 前)

经转换的常量表达式

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

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

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

(C++14 起)

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

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

(C++17 起)
(C++20 起)

常量表达式

常量表达式(constant expression)

  • 指代下列之一的泛左值核心常量表达式
  • 拥有静态存储期且非临时的对象,或
  • 拥有静态存储期的临时对象,但其值满足下文对纯右值的制约,或
  • 立即 (C++20 起)函数
  • 其值满足下列制约的纯右值核心常量表达式
  • 若其值是类类型对象,则其每个引用类型的非静态数据成员均指代满足上述对泛左值制约的实体
  • 若其值为指针类型,则它保有
  • 拥有静态存储期的对象的地址
  • 拥有静态存储期的对象的末尾后一位置的地址
  • 立即 (C++20 起)函数的地址
  • 空指针值
  • 若其值是成员函数指针类型,则它不代表立即函数 (C++20 起)
  • 若其值具有类或数组类型,则每个子对象均满足这些对值的制约
(C++14 起)

常量表达式是字面常量表达式,引用常量表达式,或地址常量表达式。

字面常量表达式是非指针字面类型(经语境所要求的转换后)的纯右值核心常量表达式。数组或类类型的字面常量表达式要求每个子对象以常量表达式初始化。

引用常量表达式是指代具有静态存储期的对象或指代函数的左值核心常量表达式。

地址常量表达式std::nullptr_t 类型或指针类型(经语境所要求的转换后)的纯右值核心常量表达式,它指向具有静态存储期的对象,指向具有静态存储期的数组末尾后一位置,指向函数,或为空指针。

(C++14 前)
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 是纯右值常量表达式
}

注意

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

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

(C++14 起)

缺陷报告

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

DR 应用于 出版时的行为 正确行为
CWG 2167 C++14 求值中局部的非成员引用会令求值为非 constexpr 允许非成员引用
CWG 1313 C++11 容许未定义行为,且禁止所有指针减法 同数组内的指针减法 OK ,禁止 UB
CWG 1952 C++11 要求诊断标准库未定义行为 未指定是否诊断库 UB

参阅

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