Modern Effective C++ item 15:尽可能的使用constexpr

ops/2024/11/29 17:54:55/
constexpr表达式/对象/函数

constexpr表达式值不会改变并且在编译过程就能得到计算结果的表达式。声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化:

constexpr int mf = 20;  //20是常量表达式
constexpr int limit = mf + 1;// mf + 1是常量表达式
constexpr int sz = size(); //之后当size是一个constexpr函数时才是一条正确的声明语句

constexpr声明中如果定义了一个指针,限定符conxtexpr仅对指针有效,与指针所指的对象无关。

const int*p=nullptr;
constexpr int* q = nullptr;  

p是一个指向常量的指针,q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。

constexpr 函数

constexpr还能定义一个常量表达式函数,即constexpr函数,常量表达式函数的返回值可以在编译

阶段就计算出来。当constexpr应用于函数时,表示函数可以在编译期执行,因为参数也是编译期常量。即使某些参数在运行时才确定,constexpr函数也能正常工作,这时它就相当于普通的函数,在运行时计算结。C++11对constexpr函数的实现有严格限制,仅允许单行return语句,但可以通过递归和三元运算符来增加表达力。到了C++14,限制被大幅放宽,允许更复杂的逻辑,包括循环和局部变量(如下)。适合于那些需要在编译期确定值的情况,例如计算数组大小或作为模板参数等。

约束规则[C++14]:

函数体允许声明变量,但是不允许static和thread_local变量。允许if/switch,不能使用goto语句。允许循环语句,包括for/while/do-while、函数可以修改生命周期和常量表达式相同的对象。 函数的返回值可以声明为void。constexpr声明的成员函数不再具有const属性。

规则约束[c++20前]

a. 必须非虚;
b. 函数体不能是函数 try 块; [c++20前]
c. 不能是协程; [c++20起]
d. 对于构造函数与析构函数 [C++20 起],该类必须无虚基类
e. 它返回类型(如果存在)和每个参数都必须是字面类型 (LiteralType)
f. 至少存在一组实参值,使得函数的一个调用为核心常量表达式的被求值的子表达式(对于构造函 数为足以用于常量初始化器) (C++14 起)。不要求诊断是否违反这点。
constexpr int pow(int base, int exp) noexcept {auto result = 1;for (int i = 0; i < exp; ++i) result *= base;return result;
}
constexpr auto numConds = 5; // 实验条件数量
std::array<int, pow(3, numConds)> results; // 结果数组,大小为3^5

定义constexpr函数pow,用于计算幂。numConds是一个constexpr变量,表示实验条件的数量。 results是一个std::array,pow(3, numConds)在编译期计算得出,用于存储实验的可能结果。

#include<iostream>
#include<array>
// 计算绝对值的 constexpr 函数
constexpr int abs_(int x) {return x > 0 ? x : -x;
}
// 计算从1到x的累加和的 constexpr 函数
constexpr int sum(int x) {int result = 0;while (x > 0) {result += x--;}return result;
}
//返回x+1的constexpr 函数
constexpr int next(int x){return ++x; // 注意,这里的 ++x 是前缀自增操作符
}
//主函数
int main() {//使用 constexpr 函数初始化数组大小char buffer1[sum(5)] = {0};//编译期计算char buffer2[abs_(-5)] = {0};//编译期计算char buffer3[next(5)] = {0};//编译期计算//使用常量表达式作为模板参数std::array<int, size(10)> arr;// 编译期计算std::cout<<"Array size:"<<arr.size()<< std::endl;//尝试使用非常量表达式作为 constexpr 函数的参数int i = 10;// 下面这行会导致编译错误,因为 'i' 不是常量表达式// constexpr int s = size(i); // 编译错误return 0;
}

编译错误:

constexpr int s = size(i);// 编译错误

导致编译错误,因为 i 是一个运行时变量,不是常量表达式。因此,size(i) 不能在编译时确定其值,不能用于初始化 constexpr 变量 s。运行时计算:

int s = size(i); // 正常工作,运行时计算

s不是 constexpr 变量,size(i) 的结果可以在运行时计算并赋值给 s。

常量表达式参数

constexpr int s_constexpr = size(10); // 正常工作,编译时计算

正常工作,因为 10 是一个常量表达式,size(10) 可以在编译时计算其结果,并用于初始化 constexpr 变量 s_constexpr。

constexpr对象

constexpr还能够修饰对象。constexpr对象本质上是const对象的加强版,它们不仅在运行时不可变,而且其值必须在编译期确定。放置在只读存储区域,适用于需要整型常量表达式(如数组大小、模板参数等)的场景。虽然所有constexpr对象都是const,但并非所有const对象都能被视为constexpr,因为后者要求其值必须在编译期可得。

#include <iostream>
struct X {int value;
};
int main(){constexpr X x = { 1 };char buffer[x.value] = { 0 };
}

以上代码自定义了一个结构体X,并且使用constexpr声明和初始化了变量x。到目前为止一切顺利,不过有时候我们并不希望成员变量被暴露出来,于是修改了X的结构:

#include <iostream>
class X {
public:X() : value(5) {}int get() const{ return value;}
private:int value;
};
int main(void){constexpr X x; //error: constexpr variable cannot have non-literal type 'const Xchar buffer[x.get()] = { 0 };//无法在编译期计算
}

解决上述问题只需要用constexpr声明X类的构造函数,即声明一个常量表达式构造函数,当然这个构造函数也有一些规则需要遵循。

1构造函数必须用constexpr声明。2构造函数初始化列表中必须是常量表达式。3构造函数的函数体必须为空(这一点基于构造函数没有返回值,所以不存在return expr。

根据这个constexpr构造函数规则修改如下

#include <iostream>
class X {
public:constexpr X():value(5){}constexpr X(int i):value{i} {}constexpr int get() const{return value;}
private:int value;
};
int main(void){constexpr X x; // error: constexpr variable cannot have non-literal type const X.char buffer[x.get()] = { 0 };
}

上面这段代码给构造函数和get函数添加constexpr说明符可以编译成功,它们本身都符合常量表达式构造函数和常量表达式函数的要求,称这样的类为字面量类类型.

在C++11中,constexpr会自动给函数带上const属性。从C++14起constexpr返回类型的类成员函数不在是const函数了。

常量表达式构造函数拥有和常量表达式函数相同的退化特性,当它的实参不是常量表达式的时候,构造函数可以退化为普通构造函数,当然,这么做的前提是类型的声明对象不能为常量表达式值。

int i=8;
constexpr X x(i); //编译失败,不能使用constexpr声明.
X y(i); //编译成功.

由于i不是一个常量,因此X的常量表达式构造函数退化为普通构造函数,这时对象x不能用constexpr声明,否则编译失败。

constexpr lambda

C++17开始,lambda表达式在条件允许的情况下(常量表达式函数的规则)都会隐式声明为constexpr。

#include <iostream>
#include <array>
constexpr int foo(){return [](){return 58;}();
}
auto get_size =[](int i) {return i * 2;};
int main(void){std::array<int,foo()> arr1= { 0 };std::array<int, get_size(5)> arr2= { 0 };
}

lambda表达式却可以用在常量表达式函数和数组长度中,可见该lambda表达式的结果在编译阶段已经计算出来了。实际上这里的[](int i) { return i * 2; }相当于:

class GetSize {
public:constexpr int operator() (int i) const {return i * 2;}
};

当lambda表达式不满足constexpr的条件时,lambda表达式也不会出现编译错误,它会作为运行时lambda表达式存在。

// 情况1
int i = 5;
auto get_size = [](int i) {return i * 2;};
char buffer1[get_size(i)] = {0}; //编译失败,get_size需要运行时调用
int a1 = get_size(i);
// 情况2
auto get_count = []() {static int x = 5;return x;
};
int a2 = get_count();

情况1和常量表达式函数相同,get_size可能会退化为运行时lambda表达式对象。当这种情况发生的时候,get_size的返回值不再具有作为数组长度的能力,但是运行时调用get_size对象还是没有问题的。

  • get_size 是一个普通的 lambda 表达式,它依赖于传入的参数 i,并在运行时返回 i * 2。当尝试将 get_size(i) 用作数组的大小时,编译器需要在编译时确定数组的大小(这是 C++ 标准要求的)。然而,由于 get_size 不是 constexpr,它不能在编译时进行求值,所以 get_size(i) 不能用于数组的大小。这会导致编译错误,因为get_size(i)需要在编译时计算,它只是一个运行时 lambda,编译器无法在编译时计算出它的结果。
  • 可以强制要求lambda表达式是一个常量表达式,用constexpr声明。做好处是可以检查lambda表达式是否有可能是一个常量表达式,如果不能则会编译报错。
auto get_size = [](int i) constexpr -> int { return i*2; };
char buffer2[get_size(5)] = { 0 };
auto get_count = []() constexpr -> int 
{static int x = 5; // 编译失败,x是一个static变量return x;
};
int a2 = get_count();

get_count 是一个 lambda 表达式,它包含一个静态局部变量 xstatic 变量在第一次调用时初始化,并在之后的每次调用中保持其值。所以 get_count 返回的值是 x,而 x 是静态的,因此 get_count() 会返回一个固定值。static变量并不符合constexpr的要求,因为constexpr需要在编译时就能求值,而static变量的生命周期是运行时管理的。如果尝试将这个 lambda 声明为constexpr,会导致编译失败。


http://www.ppmy.cn/ops/137705.html

相关文章

(超详细图文详情)Navicat 配置连接 Oracle

1、下载依赖文件 Oracle官网下载直链&#xff1a;https://www.oracle.com/database/technologies/instant-client/winx64-64-downloads.html 夸克网盘下载&#xff08;oracle19c版本&#xff09;&#xff1a;https://pan.quark.cn/s/5061e690debc 官网下载选择对应 Oracle 版…

Spring Boot教程之十二: Spring – RestTemplate

Spring – RestTemplate 由于流量大和快速访问服务&#xff0c;REST API越来越受欢迎。REST 不是一种协议或标准方式&#xff0c;而是一组架构约束。它也被称为 RESTful API 或 Web API。当发出客户端请求时&#xff0c;它只是通过 HTTP 将资源状态的表示传输给请求者或端点。传…

网络原理(一)—— http

什么是 http http 是一个应用层协议&#xff0c;全称为“超文本传输协议”。 http 自 1991 年诞生&#xff0c;目前已经发展为最主流使用的一种应用层协议。 HTTP 往往基于传输层的 TCP 协议实现的&#xff0c;例如 http1.0&#xff0c;http1.0&#xff0c;http2.0 http3 是…

霍夫变换:原理剖析与 OpenCV 应用实例

简介&#xff1a;本文围绕霍夫变换相关内容展开&#xff0c;先是讲解霍夫变换基本原理&#xff0c;包含从 xy 坐标系到 kb 坐标系及极坐标系的映射等。接着介绍了 cv2.HoughLines、cv2.HoughLinesP 概率霍夫变换、cv2.HoughCircles 霍夫圆变换的函数用法、参数含义、与常规霍夫…

基于java web的网上书店系统设计

摘 要 随着互联网的越发普及&#xff0c;网上购物成为了当下流行的热门行为。网络上开店创业有许多的优势&#xff1a;投入少&#xff0c;启动 资金低&#xff0c;交易便捷。网上书店与传统的线下书店比起来优势巨大&#xff0c;网上书店的经营方式和销售渠道是不同与线下书 店…

Python 爬虫入门教程:从零构建你的第一个网络爬虫

网络爬虫是一种自动化程序&#xff0c;用于从网站抓取数据。Python 凭借其丰富的库和简单的语法&#xff0c;是构建网络爬虫的理想语言。本文将带你从零开始学习 Python 爬虫的基本知识&#xff0c;并实现一个简单的爬虫项目。 1. 什么是网络爬虫&#xff1f; 网络爬虫&#x…

单片机知识总结(完整)

1、单片机概述 1.1. 单片机的定义与分类 定义&#xff1a; 单片机&#xff08;Microcontroller Unit&#xff0c;简称MCU&#xff09;是一种将微处理器、存储器&#xff08;包括程序存储器和数据存储器&#xff09;、输入/输出接口和其他必要的功能模块集成在单个芯片上的微型…

蓝桥杯不知道叫什么题目

小蓝有一个整数&#xff0c;初始值为1&#xff0c;他可以花费一些代价对这个整数进行变换。 小蓝可以花贵1的代价将教数增加1。 小蓝可以花费3的代价将整数增加一个值,这个值是整数的数位中最大的那个(1到9) .小蓝可以花费10的代价将整数变为原来的2倍, 例如&#xff0c;如果整…