如何制作手机网站,网络公司注册流程,站长工具seo综合查询网,给企业做网站的好处为了解决常量无法确定的问题#xff0c;C11在新标准中提出了关键字constexpr#xff0c;它能够有效地定义常量表达式#xff0c;并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。
一、常量的不确定性
在C11标准以前#xff0c;我们没有一种方法能够有效地要求一…为了解决常量无法确定的问题C11在新标准中提出了关键字constexpr它能够有效地定义常量表达式并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。
一、常量的不确定性
在C11标准以前我们没有一种方法能够有效地要求一个变量或者函数在编译阶段就计算出结果。由于无法确保在编译阶段得出结果导致很多看起来合理的代码却引来编译错误。这些场景主要集中在需要编译阶段就确定的值语法中比如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::tupleint, char tp std::make_tuple(4, 3);
// 非类型的模板参数
int x1 std::getindex0(tp);
char x2 std::getindex1(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::tupleint, char tp std::make_tuple(4, 3);
int x1 std::getindex0(tp);
char x2 std::getindex1(tp);上述代码无法通过编译因为宏定义的函数调用和const变量是在运行时确定的。
为了解决以上常量无法确定的问题C11在新标准中提出了关键字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_limitsunsigned 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有点多余因为在C11中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);六、C14对常量表达式的增强
C14标准对常量表达式函数的改进如下 1、函数体允许声明变量除了没有初始化、static和thread_local变量 2、函数允许出现if和switch语句不能使用go语句 3、函数允许所有的循环语句包括for、while、do-while 4、函数可以修改生命周期和常量表达式相同的对象 5、函数的返回值可以声明为void 6、constexpr声明的成员函数不再具有const属性 在C11中无法成功编译的常量表达式函数在C14中可以编译成功了
// 基于规则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和规则6set函数也能成功地修改x1的值了。
七、constexpr lambda表达式
从C17开始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; };