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

news2024/9/20 18:35:39

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1679297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为什么Python中会有集合set类型?

知乎上有人提问&#xff0c;为什么Python有了列表list、元组tuple、字典dict这样的容器后&#xff0c;还要弄个集合set&#xff1f; 确实set和list、tuple、dict一样&#xff0c;都是python的主要数据类型&#xff0c;它们的作用是不同的。 因为set是数学意义上的集合&#xf…

战网国际服加速器用哪个好 暴雪战网好用加速器介绍

战网国际版&#xff0c;又称Battle.net环球版&#xff0c;是暴雪娱乐操盘的全球性游戏互动平台&#xff0c;它跨越地理界限&#xff0c;服务于全球游戏爱好者。与地区限定版本相异&#xff0c;国际版赋予玩家自由进入暴雪旗下众多经典游戏的权利&#xff0c;无论身处何方&#…

使用C++实时读取串口数据(window使用已编译LibModbus库并用QT实现一个实时读取串口数据)

Modbus协议简介&#xff1a; 在工业自动化领域&#xff0c;Modbus协议是一个应用层通讯协议&#xff0c;它为电子设备之间的通信提供了一个常用的标准方法。Modbus协议最初由Modicon公司&#xff08;现为施耐德电气的一部分&#xff09;在1979年发明用于其可编程逻辑控制器&…

AIGC行业现在适合进入吗

AIGC行业目前正处于快速发展阶段,市场需求正处于爆发期,上大学网&#xff08;www.sdaxue.com&#xff09;认为&#xff0c;对于有兴趣的个人或企业而言&#xff0c;现在可能是一个适合进入的时机&#xff0c;以下是具体的分析&#xff0c;供大家参考&#xff01; 一、AIGC行业前…

java医院信息系统HIS源码SaaS模式Java版云HIS系统 接口技术RESTful API + WebSocket + WebService

java医院信息系统HIS源码SaaS模式Java版云HIS系统 接口技术RESTful API WebSocket WebService 云HIS是基于云计算的医疗卫生信息系统&#xff08;Cloud-Based Healthcare Information System&#xff09;&#xff0c;它运用云计算、大数据、物联网等新兴信息技术&#xff0c;…

熬了快两个月,终于拿到了淘天后端offer!

今年的暑期实习挺难找的&#xff0c;很多同学忙了几个月到现在还没有一个offer&#xff0c;真的很常见&#xff01;没找到暑期实习的同学千万不要太焦虑&#xff0c;可以留意留意日常实习&#xff0c;日常实习也找不到&#xff0c;那就去完善自己的项目经历&#xff0c;认真准备…

软件工程期末复习(8)需求的表达方法和状态转换图

需求的表达方法 系统模型 需求分析的任务就是借助于当前系统的逻辑模型导出目标系统的逻辑模型&#xff0c;解决目标系统 “做什么” 的问题 通常软件开发项目是要实现目标系统的物理模型。目标系统的具体物理模型是由它的逻辑模型经实例化&#xff0c;即具体到某个业务领域而…

苹果电脑里面的资料为什么不能拷贝到硬盘 mac硬盘权限限制怎么解决 mac东西拷不进硬盘怎么办

你在使用Mac电脑的时候有没有遇到过文件无法拷贝的情况呢&#xff1f;这种情况多见于Mac电脑使用U盘或者移动硬盘的时候&#xff0c;不少用户都发现&#xff1a;可以正常读取U盘里的数据但是无法拷贝文件进去&#xff0c;为什么会有这种情况呢&#xff1f; 一、mac东西拷不进硬…

macbook同时配置github,gitlab,gitee

背景&#xff1a;我的本地环境之前已经配置了github与gitlab的共存&#xff0c;现在想要添加一个gitee的项目&#xff0c;我的全局的账号和邮箱配置已经配置为了gitlab的账号邮箱&#xff0c;下边新增gitee的配置时没有对这一块儿的变更。新增了gitee后&#xff0c;我也对gitla…

webpack优化构建速度示例-IgnorePlugin:

IgnorePlugin是webpack的一个内置插件&#xff0c;允许你忽略某些特定的模块或文件 webpack.config.jsconst config {entry: ./src/index.js,output: {filename: main.js},mode: development, }module.exports config;src/index.js import moment from moment console.log(mo…

汽车工厂安灯系统能够快速知晓生产现场的状况

汽车工厂是一个庞大的生产系统&#xff0c;其中有数以百计的工人、机器和设备在不断运转&#xff0c;以确保汽车的生产顺利进行。在如此复杂的生产环境中&#xff0c;安全是至关重要的&#xff0c;而安灯系统正是一个能够帮助汽车工厂快速知晓生产现场状况的重要工具。 安灯系统…

程序在银河麒麟系统下实现开机自启及创建桌面快捷方式

目录 1. 机器环境说明 2. 程序开机自启动设置 2.桌面快捷方式设置 3. 附加说明 1. 机器环境说明 机器安装的银河麒麟操作系统属性如下&#xff1a; 2. 程序开机自启动设置 第1步&#xff1a;编写一个脚本,用于自动化启动&#xff0c;为便于后文描述&#xff0c;该脚本名称…

84.网络游戏逆向分析与漏洞攻防-游戏技能系统分析-筛选与技能有关的数据包

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…

失业焦虑如何缓解心情?流静冥想

失业焦虑如何缓解心情&#xff1f;人生旅途&#xff0c;失业犹如山重水复&#xff0c;焦虑似迷雾遮望眼。古语云&#xff1a;“山不厌高&#xff0c;海不厌深。”心之向往&#xff0c;冥想便是那披荆斩棘之斧&#xff0c;如何带你走出困境&#xff1f; “静以修身”&#xff0c…

IT行业的革新力量:技术进步与未来展望

在当今时代&#xff0c;信息技术&#xff08;IT&#xff09;行业无疑是全球经济的重要推动力之一。随着数字化转型的不断深入&#xff0c;IT行业的边界正在扩大&#xff0c;它不仅包括传统的软硬件开发、网络建设和运维服务&#xff0c;还涵盖了云计算、大数据、人工智能&#…

文件系统和软硬连接

一、磁盘 磁盘可以存储大量的二进制数据&#xff0c;并且断电后也能保持数据不丢失。因此磁盘是一种永久性存储介质&#xff0c;在计算机中&#xff0c;磁盘是一个外设&#xff0c;也是唯一的机械设备。既然磁盘是一个外设&#xff0c;那么就意味着&#xff0c;磁盘和内存&…

时间序列预测:探索性数据分析和特征工程的实用指南

时间序列分析是数据科学和机器学习领域最广泛的主题之一:无论是预测金融事件、能源消耗、产品销售还是股票市场趋势&#xff0c;这一领域一直是企业非常感兴趣的领域。 随着机器学习模型的不断进步&#xff0c;使除了传统的统计预测方法(如回归模型、ARIMA模型、指数平滑)外&a…

C++语法|对象的浅拷贝和深拷贝

背景&#xff1a; 我们手写一个顺序栈&#xff0c;展开接下来的实验&#xff1a; ⭐️ this指针指向的是类在内存中的起始位置 class SeqStack { public:SqeStack(int size 10) {cout << this << "SeqStack()" << endl;pstack_ new int[size_];t…

APP封装后防止破解的全方位策略

移动应用开发完成后&#xff0c;封装&#xff08;编译打包&#xff09;是发布前的重要步骤。然而&#xff0c;一旦APP发布&#xff0c;就可能面临被逆向工程破解的风险&#xff0c;从而导致源代码泄露、数据被盗取等严重后果。 本文将介绍一系列实用的策略和技术&#xff0c;帮…

Linux初学1

Unix unix和LInux的关系 LInux的吉祥物tux Nginx Directoryhttps://mirror.iscas.ac.cn/centos/7/isos/x86_64/redhat7 网络连接 桥接模式&#xff1a;虚拟系统可以和外部系统通讯&#xff0c; 你自家里折腾当然桥接没问题&#xff0c;如果一个教室里全都用桥接&#xff1…