C++11:constexpr 与常量表达式

在 C++中,常量表达式(Constant Expressions)指的是在编译过程中由编译器就能计算得到结果的表达式。

C++ 的一些语法要求必须使用常量表达式,最常见的比如在静态数组的声明中,数组的大小必须是一个常量表达式。

char arr1[10];
char arr2[10 + 1];
int size = 5;
char arr3[size]; // Error

在这段代码中,常数 10 显然是编译时就能确定的值,因此是一个常量表达式。表达式 10+1 的值也完全可以在编译时计算出来,所以也属于常量表达式。但这里的 size 属于变量,尽管看起来在声明 arr3 时它的值也很“确定”,但普通的变量并不属于常量表达式,C++ 不允许使用变量来声明静态数组大小。

注:某些编译器比如 GCC 可以正常编译上面的代码,这是因为 GCC 扩展了 C++ 标准,支持了变长数组。变长数组(Variable Length Array,VLA)允许使用变量来初始化静态数组,C 语言从 C99 标准开始支持 VLA,但 C++ 语言标准不支持,至少截止到 C++11 不支持。GCC 在编译 C++ 代码时也支持了变长数组,因此不报错,但其他编译器比如 MSVC++ 就会报错。需要注意变长数组不属于 C++ 标准。

又比如 switch 语句中的 case 表达式,必须是一个常量表达式。

void foo(int a)
{
    int x = 3;
    switch(a)
    {
    case 1:
    case 2:
    case x: // Error
    default:
        break;
    }
}

此处的变量 x 不属于常量表达式,因此也会导致编译报错。

到目前为止,C++ 中有两种“常量性”的概念:

一种是运行时的常量性,也就是通常所说的常量,使用 const 来表示。对于使用 const 修饰的对象,指的是运行时在作用范围内不得对这个对象进行修改。比如某个函数的输入参数为 const string& str,编译器将保证函数内的代码不能修改这个 str,如果你试图修改它,编译器就会报错,但对于输入参数 str 本身而言,每次调用函数时它的值显然很可能是不同的,const 只是保证在此函数执行的过程中不再对其进行修改,因此这是一种运行时的常量性。

另一种是编译时的常量性,也就是常量表达式,指在编译过程中可以计算得到结果的表达式。

在 C++11 之前,并没有一个用于表示常量表达式的修饰符,const 修饰符经常会进行一些客串,比如在最上面的例子中,将 size 声明为 const int size = 5; 就能通过编译。在 switch-case 的例子中,将 x 声明为 const int x = 3;也能通过编译。但这只是客串,并不表示被 const 修饰的常量对象就一定是常量表达式,比如:

void foo(const int size) {
    char arr[size]; // Error
}

这里的 size 虽被 const 修饰,但显然其值不能在编译时确定,因此也不是常量表达式。

为了更明确的表示常量表达式,C++11 引入了一个新的 constexpr 修饰符,可以约束变量或者函数,表示变量的值或者函数的返回值是常量表达式,强制确保编译器可以在编译时计算出结果,如果不符合条件,编译过程就会报错。

使用 constexpr 声明常量表达式时和 const 作用是差不多的。比如,

constexpr int A = 5;
const int B = 5;

一般情况下这两句没什么区别。但是如果A被定义在全局空间中,编译器一定会为 A 产生数据。而对于 B,编译器通常不会为它单独产生任何数据,就好像所有使用B的地方都是直接写的字面量 5 一样(除非有其他代码使用了 B 的地址)。

常量表达式的类型只允许是整形,枚举和浮点数。一般情况下,编译时期的浮点常量处理是比较敏感的,因为编译环境和最终运行环境可能不同,所以编译时期的浮点精度和运行时的浮点精度就可能有差别。不过在 C++11 中,确实允许浮点类型的常量表达式,因为标准要求编译时常量表达式中浮点的精度不能低于运行时的浮点精度。

还可以使用 constexpr 来修饰函数,表示函数的返回值是常量表达式。例如,

constexpr int square(int x) { return x  x; }

这样就可以将 square 的返回值当作常量表达式了,比如 char arr[square(2)];

但要想成为 constexpr 函数,这个函数必须相当简单:就只能有一个 return 语句返回一个值。if-else 条件判断是不允许的,但可以使用三元操作符,例如,

constexpr int foo(int x) { return x > 0 ? x : 0; }

注:实际开发中,某些新的编译器可能会比较智能,允许constexpr函数中出现一些复杂的语句,不影响其编译时的计算,但这不在标准范围内,可能不具有移植性。

constexpr 函数中也可以调用其他的 constexpr 函数,比如,

constexpr int square(int x) { return x * x; }
constexpr int cube(int x) { return square(x) * x; }

另外,constexpr 函数作为常量表达式使用时必须已经定义好,也就是不能只有声明没有定义,这也很好理解,因为constexpr 函数的返回值是在编译时计算的,编译器当然需要看到函数的定义,这一点和函数模板类似。

还有一点需要补充一下,并不是说被 constexpr 修饰的函数就只能用在常量表达式中,而是也可以作为普通函数使用。但当其出现在需要常量表达式的地方,函数中的 return 语句就只能使用其他常量表达式,换言之,函数的输入实参也必须是常量表达式。例如,

constexpr int square(int x) { return x * x; }

void main()
{
    constexpr int a = 2;  
    char arr1[square(a)]; // Ok,a是常量表达式
    int b = 2;
    char arr2[square(b)]; // Error,此处需要square(b)作为常量表达式,但输入参数b不是常量表达式
    int c = square(b); // Ok,此处square作为普通函数
    int d = square(c); // Ok,同上
}

参考:

使用 Hugo 构建
主题 StackJimmy 设计