常量表达式

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

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

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

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

目录

[编辑] 定义

属于下列常量表达式类别之一的表达式是常量表达式

C++98 常量表达式类别

整数常量表达式(C++98)

在以下位置,C++ 要求表达式求值为整数或枚举常量:

满足以下所有条件的表达式是整数常量表达式

  • 它只涉及以下实体:
  • 算术类型的字面量
  • 枚举项
  • 满足以下所有条件的变量和静态数据成员:
  • 它们具有 const 限定。
  • 它们不具有 volatile 限定。
  • 它们具有整数或枚举类型。
  • 它们以常量表达式初始化。
  • 它不会使用浮点数字面量,除非这些字面量被显式转换到整数或枚举类型。
  • 它不会应用到非整数非枚举类型的转换。
  • 它不会在 sizeof 的操作数以外的地方使用以下实体:
  • 函数
  • 类对象
  • 指针
  • 引用
  • 赋值运算符
  • 自增运算符
  • 自减运算符
  • 函数调用运算符
  • 逗号运算符

其他常量表达式类别

其他表达式只有在进行常量初始化的场合才会被视为常量表达式。此类常量表达式必须是以下之一:

  • 求值为空指针值的表达式
  • 求值为空成员指针值的表达式
  • 算术常量表达式
  • 地址常量表达式
  • 引用常量表达式
  • 对于完整对象类型的地址常量表达式,并且加上或减去一个整数常量表达式
  • 成员指针常量表达式

算术常量表达式 是满足整数常量表达式的表达式,但有以下例外:

  • 浮点数常量可以不经显式转换直接使用。
  • 可以应用到浮点数类型的转换。

地址常量表达式 是满足以下所有条件的指针类型表达式:

  • 显式使用取地址运算符
  • 隐式适用具有指针类型的常量模板形参
  • 使用数组或函数类型的表达式
  • 表达式不会调用任何函数。
  • 表达式可以在不访问结果对象的前提下使用(dynamic_cast 以外的)显式指针转换和以下运算符:
  • 下标运算符
  • 间接寻址运算符
  • 取地址运算符
  • 成员访问运算符
  • 在使用下标运算符的情况下,它的其中一个操作数必须是整数常量表达式。

引用常量表达式 是满足以下所有条件的引用类型表达式:

  • 指针表示的是具有静态存储期的对象,字符串字面量或函数的左值。该对象不能是具有非简旧数据类类型的子对象。
  • 表达式不会调用任何函数。
  • 表达式可以在不访问结果对象的前提下使用(dynamic_cast 以外的)显式指针转换和以下运算符:
  • 下标运算符
  • 间接寻址运算符
  • 取地址运算符
  • 成员访问运算符
  • 在使用下标运算符的情况下,它的其中一个操作数必须是整数常量表达式。

成员指针常量表达式 是成员指针类型的表达式,其中该指针通过对有限定标识符应用取地址运算符获得,并可以前附显式成员指针转换。

(C++11 前)

以下表达式统称为常量表达式

  • 具有静态存储期的对象的地址
  • 函数的地址
  • 空指针值
(C++11 起)
(C++14 前)

以下实体是允许的常量表达式结果

  • 具有静态存储期临时对象
  • 满足下列约束的具有静态存储期的非临时对象
  • 立即(C++20 起)函数

常量表达式 要么是指代某个允许的常量表达式结果的泛左值核心常量表达式,要么是其值满足以下约束的纯右值核心常量表达式:

  • 如果值是类类型对象,那么它的每个具有引用类型的非静态数据成员指代的都是允许的常量表达式结果。
  • 如果值是标量类型对象,那么它不能具有不确定值。
  • 如果值具有指针类型,那么该值是以下之一:
  • 具有静态存储期的对象的地址
  • 具有静态存储期的对象的尾后地址
  • 非立即(C++20 起)函数的地址
  • 空指针值
  • 如果值具有成员指针类型,那么它不能表示立即函数。
(C++20 起)
  • 如果值是类类型或数组类型对象,那么它的每个子对象的值也需要满足这些约束。
(C++14 起)
(C++26 前)

常量表达式 要么是指代某个对象或非立即函数的泛左值核心常量表达式,要么是其值满足以下约束的纯右值核心常量表达式:

(C++26 起)

当确定表达式是否为常量表达式时始终会假设不进行复制消除

C++98 的常量表达式定义全部在上面的折叠盒中。以下描述适用于 C++11 以及后续 C++ 版本。

[编辑] 字面类型

字面类型 是下列之一:

(C++17 起)
  • 满足以下任一条件的联合体聚合类型:
  • 它没有变体成员
  • 它至少有一个具有无 volatile 限定的字面类型的变体成员。
  • 每个匿名联合体成员都满足以下任一条件的非联合体聚合类型:
  • 没有变体成员。
  • 至少有一个具有无 volatile 限定的字面类型的变体成员。
  • 拥有至少一个 constexpr 构造函数(模板)且非复制或移动构造函数的类型

常量表达式中只能创建具有字面类型的对象。

[编辑] 核心常量表达式

核心常量表达式 是求值过程中不会对下列任一语言构造求值的表达式:

语言构造      版本      文档
this 指针,除了在作为该表达式的一部分而求值的 constexpr 函数之中,或者在隐式或显式的类成员访问表达式之中出现 N2235
经过具有静态或线程存储期且不可用于常量表达式块变量的声明的控制流 (C++23 起) P2242R3
  1. 调用未声明为 constexpr 的函数(或构造函数)的函数调用表达式
    constexpr int n = std::numeric_limits<int>::max(); // OK:max() 是 constexpr
    constexpr int m = std::time(NULL); // 错误:std::time() 非 constexpr
  2. 调用已声明但未定义的 constexpr 函数
  3. 调用实例化无法满足constexpr 函数/构造函数要求的 constexpr 函数/构造函数模板
  4. 调用 constexpr 虚函数,所用对象的动态类型是 constexpr 未知的
  5. 会超出实现定义的极限的表达式
  6. 导致任何形式的核心语言未定义行为或错误行为(C++26 起)的操作,不包括由标准属性引入的潜在未定义行为
    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]; // 行为未定义,但未指定是否检测
  7. (C++17 前) lambda 表达式
  8. 左值到右值隐式转换,除非应用于:
    1. (可有 cv 限定的)std::nullptr_t 类型的泛左值
    2. 指代可用于常量表达式的对象的非 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 不可用于常量表达式
                        // 因为它的初始化器不是常量初始化器
      }
    3. 指代生命期始于此表达式的求值之内的非 volatile 对象的非 volatile 字面类型的泛左值
  9. 联合体的不活跃成员或它们的子对象实施的左值到右值隐式转换或修改操作(即使它与活跃成员共享公共起始序列也是如此)
  10. (C++20 起) 应用到拥有不确定值的对象的左值到右值隐式转换
  11. 对活跃成员(如果存在)是 mutable 的联合体调用隐式定义的复制/移动构造函数或复制/移动赋值运算符,除非该联合体对象的生存期始于此表达式的求值之内
  12. (C++20 前) 会更改联合体的活跃成员的赋值表达式
  13. void 指针转换到对象指针类型 T*,除非这个指针持有空指针值或者指向一个与类型 T 相似的对象(C++26 起)
  14. 操作数所指代对象的动态类型为 constexpr 未知的(C++20 起) dynamic_cast
  15. reinterpret_cast
  16. (C++20 前) 伪析构函数调用
  17. (C++14 前) 自增或自减运算符
  18. (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) 之内开始
  19. (C++20 起) 对于生存期不在此表达式的求值内开始的对象的析构函数调用或伪析构函数调用
  20. 应用到多态类型泛左值,且该泛左值所指代对象的动态类型为 constexpr 未知(C++20 起)typeid 表达式
  21. new 表达式,除非满足以下任一条件:(C++20 起)
    • 选中的分配函数是全局可替换分配函数,且它分配的存储会在此表达式的求值内解分配。
    (C++20 起)
    • 选中的分配函数是分配类型为 T 的不分配形式,并且布置实参满足以下所有条件:
    • 它指向:
    • 某个类型与 T 相似的对象,如果 T 不是数组类型,或者
    • 某个类型与 T 相似的对象的首个元素,如果 T 是数组类型。
    • 它指向存储期在此表达式的求值内开始的存储。
    (C++26 起)
  22. delete 表达式,除非它解分配的是此表达式的求值内分配的存储区域(C++20 起)
  23. (C++20 起) 协程:await 表达式yield 表达式
  24. (C++20 起) 结果未指定的三路比较运算符
  25. 结果未指定的相等或关系运算符
  26. (C++14 前) 赋值或复合赋值运算符
  27. (C++26 前) throw 表达式
  28. (C++26 起) 异常对象的构造,除非异常对象和所有由 std::current_exceptionstd::rethrow_exception 的调用所隐式创建的副本都在此表达式的求值内部销毁
    constexpr void check(int i)
    {
        if (i < 0)
            throw i;
    }
     
    constexpr bool is_ok(int i)
    {
        try {
            check(i);
        } catch (...) {
            return false;
        }
        return true;
    }
     
    constexpr bool always_throw()
    {
        throw 12;
        return true;
    }
     
    static_assert(is_ok(5)); // OK
    static_assert(!is_ok(-1)); // C++26 起 OK
    static_assert(always_throw()); // 错误:未捕获异常
  29. 汇编声明
  30. va_arg 的调用
  31. goto 语句
  32. 会抛出异常的 dynamic_casttypeid 表达式new 表达式(C++26 起),且其中异常类型的定义不可达(C++26 起)
  33. 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 起)

[编辑] 额外要求

即使表达式 E 不会求值以上任何一项,但是如果求值它会导致运行时未定义行为,那么由实现定义 E 是否还是核心常量表达式。

即使表达式 E 不会求值以上任何一项,但是如果求值它会导致求值以下任何一项,那么未指定 E 是否还是核心常量表达式:

为确定表达式是否为核心常量表达式,在 T 是字面类型时会忽略对 std::allocator<T> 的成员函数体的求值。

为确定表达式是否为核心常量表达式,对联合体的平凡的复制/移动构造函数和复制/移动赋值运算符的调用会被视为复制/移动该联合体的活跃成员,如果存在。

为确定表达式是否为核心常量表达式,对命名了结构化绑定 bd 的标识表达式的求值具有以下语义:

  • 如果 bd 是指代绑定到虚设引用 ref 的对象的左值,那么行为如同指名 ref
  • 否则,如果 bd 命名了数组元素,那么行为就是求值 e[i] 的行为,其中 e 是以该结构化绑定声明的初始化器初始化的变量的名字,ibd 指代的元素的索引。
  • 否则,如果 bd 命名了类成员,那么行为就是求值 e.m 的行为,其中 e 是以该结构化绑定声明的初始化器初始化的变量的名字,mbd 指代的成员的名字。
(C++26 起)

在表达式作为核心常量表达式的求值过程中,所有标识表达式和 *this 的使用,如果它指代的是在该表达式求值之外已经开始生存期的对象或引用,那么都被当做指代这个对象或引用的特定实例,且它和它的所有子对象(包括所有联合体成员)的生存期包含整个常量求值。

  • 对于不可用于常量表达式的这种对象,对象的动态类型即是 constexpr 未知的。
  • 对于不可用于常量表达式的这种引用,将引用当做绑定到某个未指明的被引用类型的对象,它和它的所有子对象的生存期包含整个常量求值,且它的动态类型是 constexpr 未知的。

[编辑] 整数常量表达式

整数常量表达式 是隐式转换成纯右值的整数类型或无作用域枚举类型的表达式,其中被转换的表达式是核心常量表达式。

如果期待整数常量表达式的地方使用类类型表达式,那么该表达式将被按语境隐式转换成整数类型或无作用域枚举类型。

[编辑] 经转换的常量表达式

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

(C++17 起)

如果发生了任何引用绑定,那么它只能是直接绑定

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

(C++14 起)
(C++26 起)

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

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

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


成分实体

对象 obj成分值 定义如下:

对象 obj成分引用 包含以下引用:

  • obj 的所有具有引用类型的直接成员
  • obj 的除不活跃的联合体成员以外的所有直接子对象的成分引用

变量 var成分值 成分引用 定义如下:

  • 如果 var 声明的是对象,那么成分值和成分引用分别是该对象的成分值和成分引用。
  • 如果 var 声明的是引用,那么成分引用就是该引用。

对于变量 var 的任何成分引用 ref,如果 ref 绑定到了一个生存期延长到与 ref 相同的某个临时对象或其子对象,那么该临时对象的成分值和成分引用也递归地是 var 的成分值和成分引用。

可 constexpr 表示的实体

具有静态存储期的对象在程序的任何一点都可 constexpr 引用

对于具有自动存储期的对象 obj,如果变量 var 的最小外围作用域和点 P 的最小外围作用域是同一个不与 requires 表达式关联的函数形参作用域,那么 objP 可 constexpr 引用,其中 var 是与 obj 的完整对象对应的变量或 obj 的生存期延长到的变量。

如果满足以下所有条件,那么对象或引用 x 在点 P 可 constexpr 表示

  • 对于 x 的每个指向对象的成分值,该对象都在 P 可 constexpr 引用。
  • 对于 x 的每个指向对象尾后的成分值,该对象都在 P 可 constexpr 引用。
  • 对于 x 的每个指代对象的成分引用,该对象都在 P 可 constexpr 引用。
(C++26 起)

以常量初始化的实体

如果满足以下所有条件,那么变量或临时对象 obj 以常量初始化

(C++26 前)

如果满足以下所有条件,那么变量 obj 可以常量初始化

  • 它的初始化的完整表达式在要求常量表达式的语境下是常量表达式,其中所有契约断言都使用“忽略”求值语义。
  • var 的初始化声明后,var 声明的对象或引用立即变得可 constexpr 表示。
  • 如果 var 声明的对象或引用 x 具有静态或线程存储期,那么 x 在立即作用域是紧随 var 的初始化声明的命名空间作用域的最近点变得可 constexpr 表示。

可以常量初始化的变量在它有初始化器或它的类型可 const 默认构造以常量初始化

(C++26 起)

可用于常量表达式

constexpr 变量和具有引用类型或有 const 限定且无 volatile 限定的整数或枚举类型的变量是潜在常量的

对于以常量初始化的潜在常量的变量 var 和点 P,如果 var 的初始化声明 DP 可及,并且满足以下任意条件,那么 varP 可用于常量表达式

  • varconstexpr 变量。
  • var 没有以翻译单元局部值初始化。
  • PD 在相同的翻译单元中。

以下对象或引用在点 P 可用于常量表达式

  • P 可用于常量表达式的变量
  • 具有有 const 限定且无 volatile 限定的整数或枚举类型且生存期被延长到某个在 P 可用于常量表达式的变量的生存期的临时对象
  • 模板形参对象
  • 字符串字面量对象
  • 以上实体的非可变子对象
  • 以上实体的引用成员
(C++26 前)

以下对象或引用在点 P 可潜在用于常量表达式

  • P 可用于常量表达式的变量
  • 具有有 const 限定且无 volatile 限定的整数或枚举类型且生存期被延长到某个在 P 可用于常量表达式的变量的生存期的临时对象
  • 模板形参对象
  • 字符串字面量对象
  • 以上实体的非可变子对象
  • 以上实体的引用成员

在点 P 可潜在用于常量表达式且可 constexpr 表示的对象或引用在 P 可用于常量表达式。

(C++26 起)

明显常量求值的表达式

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

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

(C++20 起)

[编辑] 常量求值所需要的函数与变量

下列表达式或转换会潜在常量求值

如果函数是 constexpr 函数且被潜在常量求值的表达式所指名,那么它被常量求值所需要

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

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

[编辑] 常量子表达式

常量子表达式 是在作为表达式 e子表达式求值时不会阻止 e 成为核心常量表达式的表达式,其中 e 不是以下任何表达式:

(C++20 起)

[编辑] 注解

功能特性测试宏 标准 功能特性
__cpp_constexpr_in_decltype 201711L (C++20)
(DR11)
被常量求值所需要时,生成函数或变量的定义
__cpp_constexpr_dynamic_alloc 201907L (C++20) constexpr 函数中的动态存储期操作
__cpp_constexpr 202306L (C++26) void* 进行 constexpr 转型:走向 constexpr 类型擦除
202406L (C++26) constexpr 的布置 newnew[]
__cpp_constexpr_exceptions 202411L (C++26) constexpr 异常

[编辑] 示例

[编辑] 缺陷报告

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

缺陷报告 应用于 出版时的行为 正确行为
CWG 94 C++98 算术常量表达式不能涉及变量和静态数据成员 可以涉及
CWG 366 C++98 涉及字符串字面量的表达式可以是整数常量表达式 不可以是
CWG 457 C++98 涉及 volatile 变量的表达式可以是整数常量表达式 不可以是
CWG 1293 C++11 不明确字符串字面量是否可用于常量表达式 可用于常量表达式
CWG 1311 C++11 可以在常量表达式中使用 volatile 泛左值 禁止使用
CWG 1312 C++11 已禁止在常量表达式中使用 reinterpret_cast
但是转换到 void* 再转换到其他类型可以达到一样的效果
禁止从 cv void* 类型
转换到对象指针类型
CWG 1313 C++11 容许未定义行为;且禁止所有指针减法 禁止未定义行为;允许同数组内的指针减法
CWG 1405 C++11 可用于常量表达式的对象的可变子对象也可用于常量表达式 不可用于常量表达式
CWG 1454 C++11 不能通过 constexpr 函数以引用传递常量 已允许
CWG 1455 C++11 经转换的常量表达式只能是纯右值 可以是左值
CWG 1456 C++11 地址常量表达式不能表示数组末尾后一位置 可以表示
CWG 1535 C++11 操作数是/具有多态类类型的 typeid 表达式即使
不会涉及运行时检查也不是核心常量表达式
操作数限制仅限于
多态类类型的泛左值
CWG 1581 C++11 未要求定义或实例化常量求值所需要的函数 已要求
CWG 1613 C++11 核心常量表达式可以在 lambda 表达式中求值任意 ODR 使用的引用 不能求值某些引用
CWG 1694 C++11 绑定临时量的值到静态存储期引用是常量表达式 它不是常量表达式
CWG 1872 C++11 核心常量表达式可以调用不满足 constexpr 函数
要求的 constexpr 函数模板实例化
不可调用此类实例化
CWG 1952 C++11 要求诊断标准库未定义行为 未指定是否诊断库未定义行为
CWG 2022 C++98 确定常量表达式的结果可能会取决于是否进行复制消除 假设始终会进行复制消除
CWG 2126 C++11 具有有 const 限定的字面类型且生存期因
常量初始化延续的临时量不能用于常量表达式
可以用于常量表达式
CWG 2129 C++11 整数字面量不是常量表达式 是常量表达式
CWG 2167 C++11 求值中局部的非成员引用会令求值为非 constexpr 允许非成员引用
CWG 2278 C++98 CWG 问题 2022 的解决方案无法实现 假设始终不会进行复制消除
CWG 2299 C++14 不明确 <cstdarg> 中的宏能否用于常量求值 禁止 va_arg,未指定 va_start
CWG 2400 C++11 包含对 constexpr 虚函数的函数调用的表达式在调用所用的对象不可
用于常量表达式且它的生命期在此表达式之外开始时也可以是常量表达式
它不是常量表达式
CWG 2490 C++20 常量求值中的(伪)析构函数调用缺少限制 添加了限制
CWG 2552 C++23 在求值核心常量表达式时,控制流不可以经过非块变量的声明 可以经过
CWG 2558 C++11 不确定值可以是常量表达式 不是常量表达式
CWG 2647 C++20 volatile 限定类型的变量是潜在常量的 不是潜在常量的
CWG 2763 C++11 在常量求值中不需要检测是否违背了 [[noreturn]] 需要检测
CWG 2851 C++11 经转换的常量表达式不允许浮点数转换 允许非窄化浮点数转换
CWG 2907 C++11 核心常量表达式不可以对 std::nullptr_t 泛左值引用左值到右值转换 可以应用此类转换
CWG 2909 C++20 不带初始化器的变量只有在它的默认初始化
实际进行了某些初始化时才能以常量初始化
只有在它的类型可 const
默认构造时才能以常量初始化
CWG 2924 C++11
C++23
未指定违背 [[noreturn]](C++11)或
[[assume]](C++23)的约束的表达式是否还是核心常量表达式
由实现定义
P2280R4 C++11 包含标识表达式或 *this 且所指代的对象或引用的
生存期在此次求值之外开始的表达式不是常量表达式
可以是常量表达式

[编辑] 参阅

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