为了解决常量无法确定的问题,C++11在新标准中提出了关键字constexpr
,它能够有效地定义常量表达式,并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。
一、常量的不确定性
在C++11标准以前,我们没有一种方法能够有效地要求一个变量或者函数在编译阶段就计算出结果。由于无法确保在编译阶段得出结果,导致很多看起来合理的代码却引来编译错误。这些场景主要集中在需要编译阶段就确定的值语法中,比如case
语句、数组长度、枚举成员的值以及非类型的模板参数。举个例子:
const int index0 = 0;
#define index1 1
// case语句
switch (argc) {
case index0:
std::cout << "index0" << std::endl;
break;
case index1:
std::cout << "index1" << std::endl;
break;
default:
std::cout << "none" << std::endl;
}
const int x_size = 5 + 8;
#define y_size 6 + 7
// 数组长度
char buffer[x_size][y_size] = { 0 };
// 枚举成员
enum {
enum_index0 = index0,
enum_index1 = index1,
};
std::tuple<int, char> tp = std::make_tuple(4, '3');
// 非类型的模板参数
int x1 = std::get<index0>(tp);
char x2 = std::get<index1>(tp);
const
定义的常量和宏都能在要求编译阶段确定值的语句中使用,上述代码都是有效的。但是这些代码并不可靠,C++程序员应该尽量少使用宏,因为预处理器对于宏只是简单的字符替换,完全没有类型检查。对const
定义的常量可能是一个运行时常量,这种情况下是无法在case
语句以及数组长度等语句中使用的。修改一下上述代码:
int get_index0() { return 0; }
int get_index1() { return 1;}
int get_x_size() { return 5 + 8; }
int get_y_size() { return 6 + 7; }
const int index0 = get_index0();
#define index1 get_index1()
switch (argc)
{
case index0:
std::cout << "index0" << std::endl;
break;
case index1:
std::cout << "index1" << std::endl;
break;
default:
std::cout << "none" << std::endl;
}
const int x_size = get_x_size();
#define y_size get_y_size()
char buffer[x_size][y_size] = { 0 };
enum {
enum_index0 = index0,
enum_index1 = index1,
};
std::tuple<int, char> tp = std::make_tuple(4, '3');
int x1 = std::get<index0>(tp);
char x2 = std::get<index1>(tp);
上述代码无法通过编译,因为宏定义的函数调用和const
变量是在运行时确定的。
为了解决以上常量无法确定的问题,C++11在新标准中提出了关键字constexpr
,它能够有效地定义常量表达式,并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。
二、constexpr值
constexpr
值即常量表达式值,是一个用constexpr
说明符声明的变量或者数据成员,它要求该值必须在编译期计算。另外,常量表达式值必须被常量表达式初始化。
constexpr int x = 42;
char buffer[x] = { 0 };
从上述代码看,constexpr
和const
是没有区别的,将关键字替换为const
同样能达到目的。但是const
并没有确保编译期常量的特性,所以在下面的代码中,它们会有不同的表现:
int x1 = 42;
const int x2 = x1; // 定义和初始化成功
char buffer[x2] = { 0 }; // 编译失败,x2无法作为数组长度
在上面这段代码中,虽然x2
初始化编译成功,但是编译器并不一定把它作为一个编译期需要确定的值,所以在声明buffer
的时候会编译错误。这里是不一定,因为编译器的实现不一样,在GCC中,这段代码可以编译成功,但是MSVC和CLang则会编译失败。如果把const
替换为constexpr,会有不同的情况发生:
int x1 = 42;
constexpr int x2 = x1; // 编译失败,x2无法用x1初始化
char buffer[x2] = { 0 };
编译器编译第二句代码的时候就会报错,常量表达式值必须由常量表达式初始化,而x1
并不是常量,明确地违反了constexpr
的规则,编译器自然就会报错。可以看出,constexpr
约束更强,它不仅要求常量表达式是常量,并且要求是一个编译阶段就能够确定其值的常量。
三、constexpr函数
常量表达式函数的返回值可以在编译阶段就计算出来。不过在定义常量表示函数时有更多的约束规则。
1、函数必须返回一个值,所以它的返回值类型不能是
void
。2、函数体必须只有一条语句:
return expr
,其中expr
必须也是一个常量表达式。如果函数有形参,则将形参替换到expr
中后,expr
仍然必须是一个常量表达式。3、函数使用之前必须有定义。
4、函数必须用
constexpr
声明。
constexpr int max_unsigned_char() { return 0xff; }
constexpr int square(int x) { return x * x; }
constexpr int abs(int x) { return x > 0 ? x : -x; }
int main() {
char buffer1[max_unsigned_char()] = { 0 };
char buffer2[square(5)] = { 0 };
char buffer3[abs(-8)] = { 0 };
}
上述代码定义了三个常量表达式函数,由于它们的返回值能够在编译期计算出来,因此可以直接将这些函数的返回值使用在数组长度的定义上。由于标准规定函数体中只能有一个表达式return expr
,因此是无法使用if
语句的,不过用条件表达式也能完成类似的效果。
让我们看一些错误的实例
// 返回void
constexpr void foo() { }
// 不是一个常量表达式,试图修改x的值
constexpr int next(int x) { return ++x; }
// g()不是一个常量表达式
int g() { return 42; }
constexpr int f() { return g(); }
// 只有声明没有定义
constexpr int max_unsigned_char2();
enum {
max_uchar = max_unsigned_char2()
}
// 存在多条语句
constexpr int abs2(int x) {
if (x > 0) {
return x;
} else {
return -x;
}
}
// 存在多条语句
constexpr int sum(int x)
{
int result = 0;
while (x > 0)
{
result += x--;
}
return result;
}
有了常量表达式函数的支持,C++标准对STL也做了一些改进,比如在<limits>
中增加了constexpr
声明,因此下面的代码也可以顺利编译成功了:
char buffer[std::numeric_limits<unsigned char>::max()] = { 0 };
四、constexpr构造函数
constexpr
还能够声明用户自定义类型,例如:
struct X {
int x1;
};
constexpr X x = { 1 };
char buffer[x.x1] = { 0 };
上面的代码可以通过编译,constexpr
声明和初始化了变量x
。不过有时候我们不希望将变量暴露出来:
class X {
public:
X() : x1(5) {}
int get() const {
return x1;
}
private:
int x1;
};
constexpr X x; // 编译失败,X不是字面类型
char buffer[x.get()] = { 0 }; // 编译失败,x.get()无法在编译阶段计算
constexpr
说明符不能用来声明自定义类型。解决这个问题只需要用constexpr
声明X
类的构造函数,当然这个构造函数也有一些规则需要遵循:
1、构造函数必须用
constexpr
声明。2、构造函数初始化列表中必须是常量表达式。
3、构造函数的函数体必须为空(这一点基于构造函数没有返回值,所以不存在
return expr
)。
改写上述代码
class X {
public:
constexpr X() : x1(5) {}
constexpr X(int i) : x1(i) {}
constexpr int get() const {
return x1;
}
private:
int x1;
};
constexpr X x;
char buffer[x.get()] = { 0 };
上述代码给构造函数和get
函数添加了constexpr
说明符就可以编译成功,因为它们本身都符合常量表达式构造函数和常量表达式函数的要求,我们称这样的类为字面量类类型(literal class type)。其实代码中constexpr int get()const
的const
有点多余,因为在C++11中,constexpr
会自动给函数带上const
属性。
常量表达式构造函数拥有和常量表达式函数相同的退化特性,当它的实参不是常量表达式的时候,构造函数可以退化为普通构造函数,当然,这么做的前提是类型的声明对象不能为常量表达式值:
int i = 8;
constexpr X x(i); // 编译失败,不能使用constexpr声明
X y(i); // 编译成功
由于i
不是一个常量,因此X
的常量表达式构造函数退化为普通构造函数,这时对象x
不能用constexpr
声明,否则编译失败。
使用constexpr
声明自定义类型的变量,必须确保这个自定义类型的析构函数是平凡的,否则也是无法通过编译的。平凡析构函数必须满足下面3个条件。
1.自定义类型中不能有用户自定义的析构函数。
2.析构函数不能是虚函数。
3.基类和成员的析构函数必须都是平凡的。
五、对浮点的支持
constexpr
支持声明浮点类型的常量表达式值,而且标准还规定其精度必须至少和运行时的精度相同:
constexpr double sum(double x) { return x > 0 ? x + sum(x - 1) : 0; }
constexpr double x = sum(5);
六、C++14对常量表达式的增强
C++14标准对常量表达式函数的改进如下:
1、函数体允许声明变量,除了没有初始化、
static
和thread_local
变量
2、函数允许出现if
和switch
语句,不能使用go
语句
3、函数允许所有的循环语句,包括for
、while
、do-while
4、函数可以修改生命周期和常量表达式相同的对象
5、函数的返回值可以声明为void
6、constexpr
声明的成员函数不再具有const
属性
在C++11中无法成功编译的常量表达式函数,在C++14中可以编译成功了:
// 基于规则2
constexpr int abs2(int x) {
if (x > 0) {
return x;
} else {
return -x;
}
}
// 基于规则1和规则3
constexpr int sum(int x) {
int result = 0;
while (x > 0) {
result += x--;
}
return result;
}
// 基于规则4
constexpr int next(int x) {
return ++x;
}
同样这些改进也会影响常量表达式构造函数
class X {
public:
constexpr X() : x1(5) {}
constexpr X(int i) : x1(0) {
if (i > 0) {
x1 = 5;
}
else {
x1 = 8;
}
}
constexpr void set(int i) { x1 = i; }
constexpr int get() const { return x1; }
private:
int x1;
};
constexpr X make_x() {
X x;
x.set(42);
return x;
}
int main() {
constexpr X x1(-1);
constexpr X x2 = make_x();
constexpr int a1 = x1.get();
constexpr int a2 = x2.get();
std::cout << a1 << std::endl;
std::cout << a2 << std::endl;
}
上述代码的运行结果是:
main
函数里的4个变量x1
、x2
、a1
和a2
都有constexpr
声明,也就是说它们都是编译期必须确定的值。首先对于常量表达式构造函数,我们发现可以在其函数体内使用if
语句并且对x1
进行赋值操作了。可以看到返回类型为void
的set
函数也被声明为constexpr
了,这也意味着该函数能够运用在constexpr
声明的函数体内,make_x
函数就是利用了这个特性。根据规则4和规则6,set
函数也能成功地修改x1
的值了。
七、constexpr lambda表达式
从C++17开始,lambda
表达式在条件允许的情况下都会隐式声明为constexpr
。这里所说的条件,即生成constexpr
函数的规则。看一个例子:
constexpr int foo() { return []() { return 58; }(); }
auto get_size = [](int i) { return i * 2; };
char buffer1[foo()] = { 0 };
char buffer2[get_size(5)] = { 0 };
当lambda
表达式不满足constexpr
的条件时,lambda
表达式也不会出现编译错误,它会作为运行时lambda
表达式存在:
// 情况1
int i = 5;
auto get_size = [](int i) { return i * 2; };
char buffer1[get_size(i)] = { 0 };
int a1 = get_size(i);
// 情况2
auto get_count = []() {
static int x = 5;
return x;
};
int a2 = get_count();
对于情况1,上述代码按理说会编译失败,但是在GCC中由于支持了变长数组,所以是可以通过编译的,但如果你尝试在严格遵循C++标准的编译器上编译这段代码例如MSVC和CLang,则会出错。
对于情况2,由于static
变量的存在,lambda
表达式对象get_count
不可能在编译期运算,因此它最终会在运行时计算。
值得注意的是,我们也可以强制要求lambda
表达式是一个常量表达式,用constexpr
去声明它即可:
auto get_size = [](int i) constexpr -> int { return i * 2; };