【C++ 学习 ㉙】- 详解 C++11 的 constexpr 和 decltype 关键字

news2024/9/24 3:22:59

目录

一、constexpr 关键字

1.1 - constexpr 修饰普通变量

1.2 - constexpr 修饰函数

1.3 - constexpr 修饰类的构造函数

1.4 - constexpr 和 const 的区别

二、decltype 关键字

2.1 - 推导规则

2.2 - 实际应用


 


一、constexpr 关键字

constexpr 是 C++11 新引入的关键字,不过在理解其具有用法和功能之前,我们需要先理解 C++ 常量表达式。

所谓常量表达式,指的是由多个(>= 1)常量组成的表达式,换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式,这也意味着,常量表达式一旦确定,其值将无法修改

实际开发中,我们经常用到常量表达式,以定义数组为例,数组的长度就必须是一个常量表达式:

int arr1[5] = { 0, 1, 2, 3, 4 };  // ok
int arr2[2 * 5] = { 0 };  // ok
// int len = 10;
// int arr3[len] = { 0 };  // error

我们知道,C++ 程序从编写完毕到执行分为四个阶段:预处理、编译、汇编和链接,得到可执行程序后就可以运行了。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以大大地提高程序的执行效率, 因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都要计算一次的时间

对于用 C++ 编写的程序,性能往往是永恒的追求,那么在实际开发中,如何才能判断一个表达式是否为常量表达式,进而获得在编译阶段即可执行的 "特权" 呢?除了人为判定外,还有我们一开始所提到的 C++11 新引入的 constexpr 关键字 。

constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。在 C++11 中,constexpr 可用于修饰普通变量、函数(包括普通函数、类的成员函数以及模板函数)以及类的构造函数

注意:获得在程序编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算

1.1 - constexpr 修饰普通变量

C++11 中,定义普通变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力

注意:使用 constexpr 修饰普通变量时,变量必须经过初始化且初始值必须是一个常量表达式

constexpr int len = 10;
int arr[len] = { 0 };  // ok

在此示例中,也可以将 constexpr 替换成 const,即

const int len = 10;
int arr[len] = { 0 };  // ok

注意:const 和 constexpr 并不相同,关于它们的区别, 后面会进行详解

1.2 - constexpr 修饰函数

constexpr 还可以用于修饰函数的返回值,这样的函数又称为 "常量表达式函数"

注意:constexpr 并非可以修饰任意函数的返回值,换句话说,一个函数要想成为常量表达式,必须满足如下三个条件:

  1. 函数必须有返回值,即函数的返回值类型不能是 void

    constexpr void func() { }  // 函数的返回值类型不能是 void

  2. 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言以外,只能包含一条 return 返回语句,且 return 返回的表达式必须是常量表达式

    constexpr int func(int x)
    {
        constexpr int y = 0;  // 函数体中只能包含一条 return 返回语句
        return 1 + 2 + x + y;
    }

    int y = 0;
    constexpr int func(int x)
    {
        return 1 + 2 + x + y;  // return 返回的表达式必须是常量表达式
    }

    #include <iostream>
    using namespace std;
    ​
    constexpr int y = 0;
    constexpr int func(int x)
    {
        return 1 + 2 + x;
    }
    ​
    int main()
    {
        int arr[func(3)] = {  0 };
        cout << sizeof(arr) << endl;
        return 0;
    }

  3. 函数在使用之前,必须有对应的定义语义。普通函数的调用只需要提前写好该函数的声明部分即可,函数的定义部分可以放在调用位置之后甚至其他文件中,但常量表达式函数在使用前,必须要有该函数的定义

    #include <iostream>
    using namespace std;
    ​
    constexpr int func(int x);
    ​
    int main()
    {
        int arr[func(3)] = {  0 };
        cout << sizeof(arr) << endl;
        return 0;
    }
    ​
    constexpr int func(int x)
    {
        return 1 + 2 + x;
    }

以上三个条件不仅对普通函数适用,对类的成员函数和模板函数也适用

但由于函数模板中的类型不确定,因此实例化后的模板函数是否符合常量表达式函数的要求也是不确定的,针对这种情况,C++11 规定:如果 constexpr 修饰的实例化后的模板函数不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数

1.3 - constexpr 修饰类的构造函数

如果想直接得到一个常量对象,也可以使用 constexpr 修饰一个构造函数,这样就可以得到一个常量构造函数。常量构造函数有一个要求:构造函数的函数体必须为空,且必须采用初始化列表的方式为各个成员赋值

#include <iostream>
using namespace std;
​
struct Person
{
    const char* _name;
    int _age;
​
    constexpr Person(const char* name, int age)
        : _name(name), _age(age)
    { }
};
​
int main()
{
    constexpr Person p{ "张三", 18 };
    cout << p._name << ":" << p._age << endl;  // 张三:18
    return 0;
}

1.4 - constexpr 和 const 的区别

在 C++11 之前只有 const 关键字,其在实际使用中经常会表现出两种不同的语义

void func(const int num)
{
    // int arr1[num] = { 0 };  // error(num 是一个只读变量,而不是常量)
    const int count = 5;
    int arr2[count] = { 0 };  // ok(count 是一个常量)
}
  1. func 函数的参数 num 是一个只读变量,其本质上仍然是变量,而不是常量

    注意:只读并不意味着不能被修改,两者之间没有必然的联系,例如

    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        int a = 520;
        const int& ra = a;
        a = 1314;
        cout << ra << endl;  // 1314
        return 0;
    }

    引用 ra 是只读的,即无法通过自身去改变自己的值,但并不意味着无法通过其他方式间接去改变,通过改变 a 的值就可以改变 ra 的值

  2. func 函数体中的 count 则被看成是一个常量,所以可以用来定义一个静态数组

    const int count = 5;
    int* ptr = (int*)&count;
    *ptr = 10;
    cout << count << endl;

    为什么输出的 count 和 *ptr 不同呢

    具体原因是 C++ 中的常量折叠(或者常量替换):将 const 常量放在符号表中,给其分配内存,但实际读取时类似于宏替换

为了解决 const 关键字的双重语义问题,C++11 引入了新的关键字 constexpr,建议凡是表达 "只读" 语义的场景都使用 const,凡是表达 "常量" 语义的场景都使用 constexpr

所以在上面的例子中,在 func 函数体中使用 const int count = 5; 是不规范的,应使用 constexpr int count = 5;


二、decltype 关键字

decltype 是 C++11 新增的一个关键字,它和 auto 一样,都用来在编译期间进行自动类型推导

decltype 是 "declare type" 的缩写,即 "声明类型"

既然有了 auto,为什么还需要 decltype 呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下,auto 用起来非常不方便,甚至压根无法使用,所以 decltype 被引入到 C++11 中。

auto 和 decltype 的语法格式:

auto varname = value;  // varname 表示变量名,value 表示赋给变量的值
decltype(exp) varname[ = value;]  // exp 表示一个表达式

auto 根据 = 右边的初始值 value 推导出变量的类型,所以使用 auto 声明的变量必须初始化;而 decltype 根据 exp 表达式推导出变量的类型,跟 = 右边的初始值 value 没有关系,所以不要求初始化

示例

#include <iostream>
using namespace std;
​
int main()
{
    int x = 0;
​
    decltype(x) y = 1;
    decltype(x + 3.14) z = 5.5;
    decltype(&x) ptr;
​
    cout << typeid(y).name() << endl;  // int
    cout << typeid(z).name() << endl;  // double
    cout << typeid(ptr).name() << endl;  // int *
​
    // 注意:
    // decltype 的推导是在编译期间完成的,
    // 它只是用于表达式类型的推导,并不会计算表达式的值
    decltype(x++) i;
    cout << x << endl;  // 0
    return 0;
}

2.1 - 推导规则

当程序员使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:

  1. 如果表达式为普通变量、普通表达式或者类成员访问表达式,那么 decltype(exp) 的类型就和表达式的类型一致

    #include <iostream>
    using namespace std;
    ​
    class Test
    {
    public:
        string _str;
        static int _i;
    };
    ​
    int Test::_i = 0;
    ​
    int main()
    {
        int x = 0;
        int& r = x;
        
        decltype(x) y = x;  // y 被推导为 int 类型
        decltype(r) z = x;  // z 被推导为 int& 类型
        ++z;
        cout << x << " " << r << " "
            << y << " " << z << endl;  // 1 1 0 1
    ​
        Test t;
        decltype(t._str) s = "hello world";  // s 被推导为 string 类型
        decltype(Test::_i) j = 10;  // j 被推导为 int 类型
        return 0;
    }
  2. 如果表达式是函数调用,那么 decltype(exp) 的类型和函数返回值一致

    #include <iostream>
    using namespace std;
    ​
    // 函数声明
    int func_int();
    int& func_int_r();
    ​
    const int func_c_int();
    const int& func_c_int_r();
    ​
    int main()
    {
        int x = 0;
    ​
        decltype(func_int()) y = x;  // y 被推导为 int 类型
        decltype(func_int_r()) z = x;  // z 被推导为 int& 类型
        ++z;
        cout << x << " " << y << " " << z << endl;  // 1 0 1
    ​
        decltype(func_c_int()) m = x;  // m 被推导为 int 类型
        ++m;
        cout << x << " " << y << " " << z << " " << m <<  endl;  // 1 0 1 2
    ​
        decltype(func_c_int_r()) n = x;  // n 被推导为 const int& 类型
        return 0;
    }

    注意:函数 func_c_int() 的返回值是一个纯右值(即在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带 const、volatile 限定符,除此之外需要忽略这两个限定符,因此 m 被推导为 int 类型,而不是 const int 类型

  3. 如果表达式是一个左值、或者被括号 () 包围,那么 decltype(exp) 的类型就是表达式类型的引用,即假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&

    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        int x = 0;
        
        decltype((x)) y = x;  // y 被推导为 int&
        ++y;
        cout << x << " " << y << endl;  // 1 1
    ​
        decltype(x = x + 1) z = x;  // z 被推导为 int&
        ++z;
        cout << x << " " << y << " " << z << endl;  // 2 2 2
        return 0;
    }

 

2.2 - 实际应用

decltype 的应用多出现在泛型编程中

#include <vector>
using namespace std;
​
template<class T>
class Test
{   
public:
    void func(T& container)
    {
        _it = container.begin();
        // do something ... ...
    }
private:
    decltype(T().begin()) _it;
    // 当 T 是普通容器,_it 为 T::iterator;
    // 当 T 是 const 容器,_it 为 T::const_iterator。
};
​
int main()
{
    vector<int> v; 
    Test<vector<int>> t1;
    t1.func(v);
​
    const vector<int> v2;
    Test<const vector<int>> t2;
    t2.func(v2);
    return 0;
}

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

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

相关文章

Spring Boot学习笔记(1)

Spring Boot学习笔记&#xff08;1&#xff09; 1.环境1.win2.mac3. IDEA 2.知识点1.Record类2.Switch开关表达式3. var和sealed4.springboot5.启用lombok 学习资料&#xff1a; 官网&#xff0c; 手册&#xff0c; 视频。 1.环境 1.win 1.下载vscode 2.安装jdk&#xff0…

求助C语言大佬:C语言的main函数参数问题

最近在敲代码的过程中&#xff0c;突发奇想&#xff0c;产生了一个疑问&#xff1a; 为什么main函数可以任由我们定义&#xff1a;可以接收一个参数、两个参数、三个参数都接接收&#xff0c;或者可以不接收&#xff1f;这是如何实现的 int main(){retrun 0; } int main (int…

移动app安全检测报告有什么作用?

移动app安全测试是一项至关重要的任务&#xff0c;它能够帮助确保移动应用程序在使用过程中不会受到各种安全威胁的侵害。在如今移动应用程序日益普及的时代&#xff0c;移动app安全测试尤为重要。移动app安全检测报告是基于专业的安全测试团队进行的全面分析后生成的&#xff…

博客积分上一万了

博客积分上一万了 继续努力&#xff0c;勇往直前。

JOSEF约瑟 JD3-40/23 JD3-70/23漏电继电器 AC220V\0.05-0.5A

JD3系列漏电继电器&#xff08;以下简称继电器&#xff09;适用于交流电压至1140V&#xff0c;频率为50Hz&#xff0c;该继电器与分励脱扣器或失压脱扣器的断路器、交流接触器、磁力启动器等组成漏电保护装置&#xff0c;作漏电和触电保护之用&#xff0c;可配备蜂鸣器、信号等…

短视频是“风口”还是“疯口”?

熟悉我的粉丝都知道&#xff0c;最近去追了下短视频的风口&#xff0c;折腾了几个视频出来。且不说视频效果如何&#xff0c;单单是制作视频的过程&#xff0c;就差点没要了童话的老命。看似短短的几分钟&#xff0c;真的应了那句话&#xff1a;台上一分钟&#xff0c;台下十年…

Ubuntu系统忘记Root用户密码-无法登录系统-更改Root密码-Ubuntu系统维护

一、背景 很多时候&#xff0c;我们总会设计复杂的密码&#xff0c;但是大多数时候&#xff0c;我们反而会先忘记我们的密码&#xff0c;导致密码不仅仅阻挡其他用户进入系统&#xff0c;同时也阻碍我们进入系统。 本文将介绍在忘记密码的情况下&#xff0c;如何进入系统并更改…

macOS Sonoma 桌面小工具活学活用!

macOS Sonoma 虽然不算是很大型的改版&#xff0c;但当中触目的新功能是「桌面小工具」&#xff08;Widget&#xff09;。如果我们的萤幕够大&#xff0c;将能够放更多不同的Widget&#xff0c;令用户无须开App 就能显示资讯&#xff0c;实在相当方便。 所有iPhone Widget 也能…

基于Springboot服装商品管理系统免费分享

基于Springboot服装商品管理系统 作者: 公众号(擎云毕业设计指南) 更多毕设项目请关注公众号&#xff0c;获取更多项目资源。如需部署请联系作者 注&#xff1a;禁止使用作者开源项目进行二次售卖&#xff0c;发现必究&#xff01;&#xff01;&#xff01; 运行环境&…

controller调用service层报错Invalid bound statement (not found)

报错信息&#xff1a; "Invalid bound statement (not found): com.gelei.system.service.TbUserFollowService.getMyUserFanList" 这个问题就很神奇&#xff0c;请看下图&#xff0c;我测试的时候就是这么个情况&#xff1b; 综上所述&#xff0c;解决方法如下&…

pragma once与ifndef的区别

概要 代码编译过程中&#xff0c;为了防止同一份代码被重复引用&#xff0c;通常有两种实现方式 方式一 #pragma once 方式二 #ifndef _TEST_H_ #define _TEST_H_ #endif // !TEST_H 通常情况下&#xff0c;使用上述两种方式中的任意一种都是可以的。最近工作中&#xff0c;代…

阿里云ECS服务器的搭建学习

云服务器ECS&#xff1a; 云服务器&#xff08;Elastic Compute Service&#xff0c;简称ECS&#xff09;是阿里云提供的性能卓越、稳定可靠、弹性扩展的IaaS&#xff08;Infrastructure as a Service&#xff09;级别云计算服务。云服务器ECS免去了您采购IT硬件的前期准备&a…

直线模组有哪些配件组成的?

直线模组又称线性模组或线性滑台&#xff0c;是自动化设备中重要的传动元件&#xff0c;主要由以下几部分组成&#xff1a; 1、直线导轨&#xff1a;直线导轨又称线性滑轨&#xff0c;是用于直线往复运动场合的重要零部件&#xff0c;它具有比直线轴承更高的额定负载&#xff0…

吉利高端品牌领克汽车携手体验家,重塑智能创新的汽车服务体验

浙江吉利控股集团&#xff08;以下简称“吉利集团”&#xff09;始建于1986年&#xff0c;1997年进入汽车行业&#xff0c;一直专注实业&#xff0c;专注技术创新和人才培养&#xff0c;坚定不移地推动企业转型升级和可持续发展。现资产总值超5100亿元&#xff0c;员工总数超过…

【内网击穿工具 】NATAPP

内网穿透又叫内网映射&#xff0c;功能是把内网IP映射到公网&#xff0c;使公网也能轻松访问所搭建的服务。 内网与外网 外网指的是一个组织或网络中可公开访问的网络&#xff0c;即对外开放的网络。外网可以通过公共互联网进行访问 内网是相对于外网而言的&#xff0c;指的…

十四、Django框架使用

目录 一、框架简介二、MVT模型简介三、Python的虚拟环境3.1 安装virtualenv 虚拟环境3.2 创建和使用虚拟环境四、Django项目的搭建4.1 安装Django包4.2 创建Django项目4.3 创建Django项目的应用4.4 使用pycharm打开Django项目4.5 注册Django项目的应用4.6 启动Django项目五、OR…

Guava-RateLimiter详解

简介&#xff1a; 常用的限流算法有漏桶算法和令牌桶算法&#xff0c;guava的RateLimiter使用的是令牌桶算法&#xff0c;也就是以固定的频率向桶中放入令牌&#xff0c;例如一秒钟10枚令牌&#xff0c;实际业务在每次响应请求之前都从桶中获取令牌&#xff0c;只有取到令牌的请…

聚观早报 | 荣耀Play8T上市;阿芙“超级品牌日”上线

【聚观365】10月19日消息 荣耀Play8T上市 阿芙“超级品牌日”上线 特斯拉家庭充电服务包更新 TikTok Shop印尼站关停 高通与谷歌合作开发RISC-V芯片 荣耀Play8T上市 3月28日&#xff0c;荣耀推出了荣耀Play 7T系列手机&#xff0c;其最大的卖点就是搭载了6000mAh大电池&a…

`SQL`编写判断是否为工作日函数编写

SQL编写判断是否为工作日函数编写 最近的自己在写一些功能,遇到了对于工作日的判断,我就看了看sql,来吧!~(最近就是好疲惫) 我们一起看看(针对ORACLE) 1.声明: CREATE OR REPLACE PACKAGE GZYW_2109_1214.PKG_FUN_GETDAY_HDAY AS /** * 通过节假日代码获取指定的日期[查找基…

开发小经验积累

今天使用langchain官方文档上的这个包的时候 遇到了这个报错 这个直觉判断肯定是版本问题&#xff0c;我先是去perplexity.ai搜了相关报错 后来没找到什么比较好的回答 这时候想到可以去看当前自己用的版本的langchain的源码&#xff0c;而利用vscode强大的功能&#xff0c;…