C++11 新特性 常量表达式 constexpr

2024-05-16 03:20

本文主要是介绍C++11 新特性 常量表达式 constexpr,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为了解决常量无法确定的问题,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 };

从上述代码看,constexprconst是没有区别的,将关键字替换为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()constconst有点多余,因为在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、函数体允许声明变量,除了没有初始化、staticthread_local变量
2、函数允许出现ifswitch语句,不能使用go语句
3、函数允许所有的循环语句,包括forwhiledo-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;
}

上述代码的运行结果是:

image-2155105029

main函数里的4个变量x1x2a1a2都有constexpr声明,也就是说它们都是编译期必须确定的值。首先对于常量表达式构造函数,我们发现可以在其函数体内使用if语句并且对x1进行赋值操作了。可以看到返回类型为voidset函数也被声明为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; };

这篇关于C++11 新特性 常量表达式 constexpr的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/993730

相关文章

Win32下C++实现快速获取硬盘分区信息

《Win32下C++实现快速获取硬盘分区信息》这篇文章主要为大家详细介绍了Win32下C++如何实现快速获取硬盘分区信息,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 实现代码CDiskDriveUtils.h#pragma once #include <wtypesbase

C++ Primer 标准库vector示例详解

《C++Primer标准库vector示例详解》该文章主要介绍了C++标准库中的vector类型,包括其定义、初始化、成员函数以及常见操作,文章详细解释了如何使用vector来存储和操作对象集合,... 目录3.3标准库Vector定义和初始化vector对象通列表初始化vector对象创建指定数量的元素值

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、

Rust中的Drop特性之解读自动化资源清理的魔法

《Rust中的Drop特性之解读自动化资源清理的魔法》Rust通过Drop特性实现了自动清理机制,确保资源在对象超出作用域时自动释放,避免了手动管理资源时可能出现的内存泄漏或双重释放问题,智能指针如B... 目录自动清理机制:Rust 的析构函数提前释放资源:std::mem::drop android的妙

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没