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

相关文章

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

C++中detach的作用、使用场景及注意事项

《C++中detach的作用、使用场景及注意事项》关于C++中的detach,它主要涉及多线程编程中的线程管理,理解detach的作用、使用场景以及注意事项,对于写出高效、安全的多线程程序至关重要,下... 目录一、什么是join()?它的作用是什么?类比一下:二、join()的作用总结三、join()怎么

Spring Boot3.0新特性全面解析与应用实战

《SpringBoot3.0新特性全面解析与应用实战》SpringBoot3.0作为Spring生态系统的一个重要里程碑,带来了众多令人兴奋的新特性和改进,本文将深入解析SpringBoot3.0的... 目录核心变化概览Java版本要求提升迁移至Jakarta EE重要新特性详解1. Native Ima