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++判断水仙花数并输出的相关资料,文中通过代码介绍的非常详细,需要的朋友可以... 以下是使用C++实现的相同逻辑代码:#include <IOStream>#include <vec

基于C++的UDP网络通信系统设计与实现详解

《基于C++的UDP网络通信系统设计与实现详解》在网络编程领域,UDP作为一种无连接的传输层协议,以其高效、低延迟的特性在实时性要求高的应用场景中占据重要地位,下面我们就来看看如何从零开始构建一个完整... 目录前言一、UDP服务器UdpServer.hpp1.1 基本框架设计1.2 初始化函数Init详解

C++ 右值引用(rvalue references)与移动语义(move semantics)深度解析

《C++右值引用(rvaluereferences)与移动语义(movesemantics)深度解析》文章主要介绍了C++右值引用和移动语义的设计动机、基本概念、实现方式以及在实际编程中的应用,... 目录一、右值引用(rvalue references)与移动语义(move semantics)设计动机1

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

C++构造函数中explicit详解

《C++构造函数中explicit详解》explicit关键字用于修饰单参数构造函数或可以看作单参数的构造函数,阻止编译器进行隐式类型转换或拷贝初始化,本文就来介绍explicit的使用,感兴趣的可以... 目录1. 什么是explicit2. 隐式转换的问题3.explicit的使用示例基本用法多参数构造

C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解

《C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解》:本文主要介绍C++,C#,Rust,Go,Java,Python,JavaScript性能对比全面... 目录编程语言性能对比、核心优势与最佳使用场景性能对比表格C++C#RustGoJavapythonjav

C++打印 vector的几种方法小结

《C++打印vector的几种方法小结》本文介绍了C++中遍历vector的几种方法,包括使用迭代器、auto关键字、typedef、计数器以及C++11引入的范围基础循环,具有一定的参考价值,感兴... 目录1. 使用迭代器2. 使用 auto (C++11) / typedef / type alias

C语言逗号运算符和逗号表达式的使用小结

《C语言逗号运算符和逗号表达式的使用小结》本文详细介绍了C语言中的逗号运算符和逗号表达式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习... 在C语言中逗号“,”也是一种运算符,称为逗号运算符。 其功能是把两个表达式连接其一般形式为:表达

C++ scoped_ptr 和 unique_ptr对比分析

《C++scoped_ptr和unique_ptr对比分析》本文介绍了C++中的`scoped_ptr`和`unique_ptr`,详细比较了它们的特性、使用场景以及现代C++推荐的使用`uni... 目录1. scoped_ptr基本特性主要特点2. unique_ptr基本用法3. 主要区别对比4. u