当前位置:   article > 正文

十一、C++2.0其他部分_强类型枚举支持数组吗

强类型枚举支持数组吗

目录

1 RAII

2 循环

2.1 基于范围的for循环(C++11)

2.2 结构化绑定(C++14)

2.2.1 绑定数组

2.2.2 绑定元组

2.2.3 绑定成员对象

3 If Statements with Initializer(带初始化器的if语句)

4 inline 变量

5 强枚举类型

5.1 传统枚举类型的缺陷

5.2 强类型枚举

5.3 C++11对传统枚举类型的扩展

6 继承构造

6.1 问题描写叙述

6.2 问题的解决

7 委托构造

7.1 委托构造函数概念

7.2 委托构造函数作用

7.3 示例代码

7.4 委派构造函数优化

7.5 注意事项

8 POD 类型

8.1 背景

8.2 POD 类型

8.2.1 平凡的

8.2.2 标准布局的

8.3 POD 类型的应用

9 非受限联合体(union)

10 reference_wrapper

10.1 是什么?

10.2 构造函数 和 赋值运算符

10.3 get成员

10.4 特殊用法

10.4.1 可以存入容器中

10.4.2 可以像普通的引用一样作为实参传入函数

10.4.3 其他

11 string_view

12 optional

13 attribute属性说明符

13.1 [[fallthrough]] 标准属性

13.2 [[maybe_unused]] 标准属性

13.3 [[nodiscard]] 标准属性

13.4 [[deprecated]] 标准属性

13.5 [[noretrun]] 标准属性

14 变量模板 Variable Templates

15 泛型lambda

16 函数返回类型自动推导

17 函数引用后置修饰符

18 C++返回值类型后置(跟踪返回值类型)

19 constexpr

19.1 constexpr修饰普通变量:

19.2 constexpr修饰函数:

19.3 constexpr修饰类的构造函数:

19.4 constexpr修饰模板函数

20 Constexpr if

21 嵌套命名空间

22 filesystem

24 any

25 variant

26 execution






1 RAII

        RAII,在c++reference(链接)的解释就是资源获取即初始化(Resource Acquisition Is Initialization),是一种C++编程技术。简单来说就是,一个类在构造函数初始化所需要的资源,构造函数释放对应的资源,利用C++局部变量自动释放的机制,可以一定程度上减少内存泄漏

        C++中设计ScopeGuard的关键技术:通过局部变量析构函数来管理资源,根据是否是正常退出来确定是否需要清理资源用C++11做很简单。

  1. #ifndef __SCOPE_GUARD_H__
  2. #define __SCOPE_GUARD_H__
  3. #define __SCOPEGUARD_CONCATENATE_IMPL(s1, s2) s1##s2
  4. #define __SCOPEGUARD_CONCATENATE(s1, s2) __SCOPEGUARD_CONCATENATE_IMPL(s1, s2)
  5. #if defined(__cplusplus)
  6. #include <type_traits>
  7. // ScopeGuard for C++11
  8. namespace clover {
  9. template <typename Fun>
  10. class ScopeGuard {
  11. public:
  12. ScopeGuard(Fun &&f) : _fun(std::forward<Fun>(f)), _active(true) {
  13. }
  14. ~ScopeGuard() {
  15. if (_active) {
  16. _fun();
  17. }
  18. }
  19. void dismiss() {
  20. _active = false;
  21. }
  22. ScopeGuard() = delete;
  23. ScopeGuard(const ScopeGuard &) = delete;
  24. ScopeGuard &operator=(const ScopeGuard &) = delete;
  25. ScopeGuard(ScopeGuard &&rhs) : _fun(std::move(rhs._fun)), _active(rhs._active) {
  26. rhs.dismiss();
  27. }
  28. private:
  29. Fun _fun;
  30. bool _active;
  31. };
  32. namespace detail {
  33. enum class ScopeGuardOnExit {};
  34. template <typename Fun>
  35. inline ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun &&fn) {
  36. return ScopeGuard<Fun>(std::forward<Fun>(fn));
  37. }
  38. } // namespace detail
  39. } // namespace clover
  40. // Helper macro
  41. #define ON_SCOPE_EXIT \
  42. auto __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) = clover::detail::ScopeGuardOnExit() + [&]()
  43. #else
  44. // ScopeGuard for Objective-C
  45. typedef void (^ext_cleanupBlock_t)(void);
  46. static inline void ext_executeCleanupBlock(__strong ext_cleanupBlock_t *block) {
  47. (*block)();
  48. }
  49. #define ON_SCOPE_EXIT \
  50. __strong ext_cleanupBlock_t __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) \
  51. __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^
  52. #endif
  53. #endif /* __SCOPE_GUARD_H__ */

2 循环

2.1 基于范围的for循环(C++11)

C++ 11 标准中,除了可以沿用前面介绍的用法外,还为 for 循环添加了一种全新的语法格式,如下所示:

  1. for (declaration : expression){
  2.     //循环体
  3. }

其中,两个参数各自的含义如下:

  • declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration参数处定义的变量类型可以用 auto 关键字表示,该关键字可以使编译器自行推导该变量的数据类型。
  • expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。

有读者可能会问,declaration 参数既可以定义普通形式的变量,也可以定义引用形式的变量,应该如何选择呢?其实很简单,如果需要在遍历序列的过程中修改器内部元素的值,就必须定义引用形式的变量;反之,建议定义const &(常引用)形式的变量(避免了底层复制变量的过程,效率更高),也可以定义普通变量。

2.2 结构化绑定(C++14)

  • 结构化绑定绑定指定名称到初始化器的子对象或元素。
  • 结构化绑定可以用于for循环,节省代码量,比如:
  1. list<tuple<int, double, string>> list;
  2. list.emplace_back(1, 9.9, "aa");
  3. for (auto [i, d, s] : list) {
  4. cout << i << " " << d << " " << s << endl;
  5. }

2.2.1 绑定数组

  1. int a[2] = 1,2;
  2. auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
  3. auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]

2.2.2 绑定元组

  1. float x;
  2. char y;
  3. int z;
  4. std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
  5. const auto& [a,b,c] = tpl;
  6. // a 指名指代 x 的结构化绑定;decltype(a) 为 float&
  7. // b 指名指代 y 的结构化绑定;decltype(b) 为 char&&
  8. // c 指名指代 tpl 的第 3 元素的结构化绑定;decltype(c) 为 const int

2.2.3 绑定成员对象

  1. struct S
  2. mutable int x1 : 2;
  3. volatile double y1;
  4. ;
  5. S f();
  6. const auto [x, y] = f(); // x 是标识 2 位位域的 int 左值
  7. // y 是 const volatile double 左值

3 If Statements with Initializer(带初始化器的if语句)

带初始化器的if语句,example:

  1. std::map<std::string, int> map;
  2. map["nihao"] = 1;
  3. map["shijie"] = 2;
  4. if (auto ret = map.begin(); ret != map.end()) {
  5. std::cout << ret->first << ": " << ret->second;
  6. }

4 inline 变量

inline 变量用来解决一个问题,全局变量再头文件中定义之后多处使用产生符号重定义错误

错误例子:

  1. // test.h 头文件
  2. int test = 10;
  3. // test1.cpp 
  4. void Function1() {
  5.   test = 20;
  6. }
  7. // test2.cpp
  8. void Function() {
  9.   test = 30;
  10. }
  11. // 上面的代码编译将会产生重定义错误,c++17之前解决方案是使用extern导出全局变量
  12. // 解决方案
  13. // test.h 头文件
  14. extern int test;
  15. // test.cpp
  16. int test = 10;
  • C++17 之后引入inline变量使其全局变量可以直接再头文件中声明定义。example:
inline int test = 10;

5 强枚举类型

5.1 传统枚举类型的缺陷

枚举类型是C/C++中用户自定义的构造类型,它是由用户定义的若干枚举常量的集合。枚举值对应整型数值,默认从0开始。比如定义一个描述性别的枚举类型。

其中枚举值Male被编译器默认赋值为0,Female赋值为1。传统枚举类型在设计上会存在以下几个问题。

enum Gender{Male,Female};

(1)同作用域同名枚举值会报重定义错误。传统C++中枚举常量被暴漏在同一层作用域中,如果同一作用域下有两个不同的枚举类型,但含有同名的枚举常量也是会报编译错误的,比如:

  1. enum Fruits{Apple,Tomato,Orange};
  2. enum Vegetables{Cucumber,Tomato,Pepper}; //编译报Tomato重定义错误

其中水果和蔬菜两个枚举类型中包含同名的Tomato枚举常量会导致编译错误。因为enum则是非强作用域类型,枚举常量可以直接访问,这种访问方式与C++中具名的namespace、class/struct以及union必须通过"名字::成员名"的访问方式大相径庭。

(2)由于枚举类型被设计为常量数值的“别名”,所以枚举常量总是可以被隐式转换为整型,且用户无法为枚举常量定义类型。

(3)枚举常量占用存储空间以及符号性不确定。C++标准规定C++枚举所基于的“基础类型”是由编译器来具体实现,这会导致枚举类型成员的基本类型存在不确定性问题,尤其是符号性问题,即。考察如下示例:

  1. enum A{A1=1,A2=2,ABig=0xFFFFFFFFU};
  2. enum B{B1=1,B2=2,BBig=0xFFFFFFFFFUL};
  3. int main()
  4. {
  5. cout<<sizeof(A1)<<endl; //4
  6. cout<<ABig<<endl; //4294967295
  7. cout<<sizeof(B1)<<endl; //8
  8. cout<<BBig<<endl; //68719476735
  9. }

以上输出结果是在Linux平台下使用g++编译输出的结果,在VC++(VS2017)中的输出结果分别是4、-1、4和-1。可见不同编译器对枚举常量的整型类型的宽度和符号有着不同的实现。GNU C++会根据枚举数值的类型使用不同宽度和符号的整型,VC++则始终以有符号int来表示枚举常量。

为了解决以上传统枚举类型的缺陷,C++11引入了强类型枚举解决了这些问题。

5.2 强类型枚举

非强作用域类型,允许隐式转换为整型,枚举常量占用存储空间以及符号性的不确定,都是枚举类缺点。针对这些缺点,C++11引入了一种新的枚举类型——强类型枚举(strong-typed enum)。

强类型枚举使用enum class语法来声明:

enum class Enumeration{VAL1,VAL2,VAL3=100,VAL4};

强类型枚举具有如下几个优点:
(1)强作用域,强类型枚举成员的名称不会被输出到其父作用域,所以不同枚举类型定义同名枚举成员编译不会报重定义错误。进而使用枚举类型的枚举成员时,必须指明所属范围,比如Enum::VAL1,而单独的VAL1则不再具有意义;
(2)转换限制,强类型枚举成员的值不可以与整型发生隐式相互转换。比如比如Enumeration::VAL4==10;会触发编译错误;
(3)可以指定底层类型。强类型枚举默认的底层类型是int,但也可以显示地指定底层类型。具体方法是在枚举名称后面加上":type",其中type可以是除wchar_t以外的任何整型。比如:

enum class Type:char{Low,Middle,High};

注意:
(1)声明强类型枚举的时候,既可以使用关键字enum class,也可以使用enum struct。事实上,enum struct与enum class在语法上没有任何区别。
(2)由于强类型枚举是强类型作用域的,故匿名的enum class可能什么都做不了,如下代码会报编译错误:

  1. enum class{General,Light,Medium,Heavy}weapon;
  2. int main()
  3. {
  4. weapon=Medium; //编译出错
  5. bool b=weapon == weapon::Medium; //编译出错
  6. return 0;
  7. }

当然对于匿名强类型枚举我们还是可以使用decltype来获得其类型并进而使用,但是这样做可能违背强类型枚举进行匿名的初衷。

5.3 C++11对传统枚举类型的扩展

传统枚举类型为了配合C++11引入的强类型枚举,C++11对传统枚举类型进行了扩展。
(1)底层的基本类型可以在枚举名称后加上":type",其中type可以是除wchar_t以外的任何整型,比如:

enum Type:char{Low,Middle,High};

(2)C++11中,枚举类型的成员可以在枚举类型的作用域内有效。比如:

  1. enum Type{Low, Middle, High };
  2. Type type1 = Middle;
  3. Type type2 = Type::Middle;

其中Middle与Type::Middle都是合法的使用形式。

以上就是详解C++11强类型枚举的详细内容,更多关于c++ 枚举的资料请关注脚本之家其它相关文章!

6 继承构造

6.1 问题描写叙述

在继承体系中,假设派生类想要使用基类的构造函数,须要在构造函数中显式声明。

例如以下:

  1. struct A
  2. {
  3. A(int i){}
  4. };
  5. struct B:A
  6. {
  7. B(int i):A(i){}
  8. };

在这里,B派生于A,B,又在构造函数中调用A的构造函数。从而完毕构造函数的传递。

又比方例如以下。当B中存在成员变量时:

  1. struct A
  2. {
  3. A(int i){}
  4. };
  5. struct B:A
  6. {
  7. B(int i):A(i),d(i){}
  8. int d;
  9. };

如今派生于A的结构体B包括一个成员变量,我们在初始化基类A的同一时候也初始化成员d。如今的问题是:假若基类用于拥有为数众多的不同版本号的构造函数。这样,在派生类中按上面的思维还得写非常多相应的"透传"构造函数。例如以下:

  1. struct A
  2. {
  3. A(int i) {}
  4. A(double d,int i){}
  5. A(float f,int i,const char* c){}
  6. //...等等系列的构造函数版本号
  7. };
  8. struct B:A
  9. {
  10. B(int i):A(i){}
  11. B(double d,int i):A(d,i){}
  12. B(folat f,int i,const char* c):A(f,i,e){}
  13. //......等等好多个和基类构造函数相应的构造函数
  14. };

非常明显当基类构造函数一多,派生类构造函数的写法就显得非常累赘,相当不方便。

6.2 问题的解决

我们能够通过using声明来完毕这个问题的简化,看一个样例

  1. struct Base
  2. {
  3. void f(double i)
  4. {
  5. cout<<"Base:"<<i<<endl;
  6. }
  7. };
  8. struct Drived:Base
  9. {
  10. using Base::f;
  11. void f(int i)
  12. {
  13. cout<<"Drived:"<<i<<endl;
  14. }
  15. };

代码中基类和派生类都声明了同名的函数f。但派生类中办法和基类的版本号不同,这里使用using声明,说明派生类中也使用基类版本号的函数f。这样派生类中就拥有两个f函数的版本号了。在这里须要说明的是,假设没有使用using声明继承父类同名函数,那么派生类中定义的f函数将会屏蔽父类的f函数,当然若派生类根本就未定义这个f同名函数。还会选择用基类的f函数。

这样的方法,我们一样可迁移到构造函数的继承上。即派生类能够通过using语句声明要在子类中继承基类的全部构造函数。

例如以下:

  1. struct A
  2. {
  3. A(int i) {}
  4. A(double d,int i){}
  5. A(float f,int i,const char* c){}
  6. //...等等系列的构造函数版本号
  7. };
  8. struct B:A
  9. {
  10. using A::A;
  11. //关于基类各构造函数的继承一句话搞定
  12. //......
  13. };

如今,通过using A::A的声明。将基类中的构造函数全继承到派生类中,更巧妙的是,这是隐式声明继承的。即假设一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。 但此时另一个问题:

当使用using语句继承基类构造函数时。派生类无法对类自身定义的新的类成员进行初始化,我们可使用类成员的初始化表达式,为派生类成员设定一个默认初始值。比方:

  1. struct A
  2. {
  3. A(int i) {}
  4. A(double d,int i){}
  5. A(float f,int i,const char* c){}
  6. //...等等系列的构造函数版本号
  7. };
  8. struct B:A
  9. {
  10. using A::A;
  11. int d{0};
  12. };

【注意】:

1.对于继承构造函数来说,參数的默认值是不会被继承的,并且,默认值会 导致基类产生多个构造函数版本号(即參数从后一直往前面减。直到包括无參构造函数,当然假设是默认复制构造函数也包括在内),这些函数版本号都会被派生类继承。

2.继承构造函数中的冲突处理:当派生类拥有多个基类时,多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名。

參数都同样,那么继承类中的继承构造函数将导致不合法的派生类代码,比方:

  1. struct A
  2. {
  3. A(int){}
  4. };
  5. struct B
  6. {
  7. B(int){}
  8. };
  9. struct C:A,B
  10. {
  11. using A::A;
  12. using B::B;
  13. };

在这里将导致派生类中的继承构造函数发生冲突,一个解决的办法就是显式的继承类的冲突构造函数。阻止隐式生成对应的继承构造函数,以免发生冲突。

  1. struct C:A,B
  2. {
  3. using A::A;
  4. using B::B;
  5. C(int){}
  6. };

3.假设基类的构造函数被声明为私有构造函数或者派生类是从基类虚继承的,那么就不能在派生类中声明继承构造函数。

4.假设一旦使用了继承构造函数,编译器就不会为派生类生成默认构造函数。这样,我们得注意继承构造函数无參版本号是不是有须要。

7 委托构造

避免你有多个参数表不同但是逻辑相近(或者有公共部分)的构造函数的时候,一个逻辑写好几遍造成代码重复。

遇到的问题:

Q:执行的顺序?

A:先执行目标构造函数,再执行委托构造函数。

7.1 委托构造函数概念

C++11 引入了委托构造的概念,某个类型的一个构造函数可以委托同类型的另一个构造函数对对象进行初始化。为了描述方便我们称前者为委托构造函数,后者为目标构造函数。委托构造函数会将控制权交给目标构造函数,在目标构造函数执行完之后,再执行委托构造函数的主体。委托构造函数的语法非常简单,只需要在委托构造函数的初始化列表中调用目标构造函数即可。

  • 委派构造函数:初始化列表中调用“基准版本”的构造函数就是委派构造函数。
  • 目标构造函数:被调用“基准版本”构造函数就是目标构造函数。

7.2 委托构造函数作用

避免你有多个参数表不同但是逻辑相近(或者有公共部分)的构造函数的时候,一个逻辑写好几遍造成代码重复。

7.3 示例代码

如何创建委托构造函数:

  1. class Person {
  2. public:
  3. // 非委托构造函数使用对应的实参初始化成员
  4. Person(std::string _name, int _age, double _income):
  5. name(_name), age(_age), income(_income) { }
  6. // 其余构造函数全都委托给另一个构造函数
  7. Person(): Person("", 0, 0) {}
  8. Person(std::string _name): Person(mike,0,0) {}
  9. Persona(std::string _name, int _age): Person(mike,20,0){}
  10. };


调用关系示例: 

  1. #include <iostream>
  2. class Data
  3. {
  4. public:
  5. int num1;
  6. int num2;
  7. Data() //目标构造函数
  8. {
  9. num1 = 100;
  10. }
  11. Data(int num) : Data() //委托构造函数
  12. {
  13. // 委托 Data() 构造函数
  14. num2 = num;
  15. }
  16. };
  17. void function()
  18. {
  19. Data data(99); //首先调用Data() 先给num1复制,然后走到Data(int num) : Data() ,给num2赋值
  20. std::cout <<data.num1 << std::endl;
  21. std::cout <<data.num2 << std::endl;
  22. }

运行结果:

100

99

7.4 委派构造函数优化

为了使用委派构造时还能使用初始化列表,我们可以定义一个private 的目标构造函数,并将初始化列表放在这个private的目标构造函数中,这样其他委派构造函数就可以通过委派这个目标构造来实现构造的功能。

  1. class Person
  2. {
  3. public:
  4. Person() :Person(1, 'a') {}
  5. Person(int i) : Person(i, 'a') {}
  6. Person(char ch) : Person(1, ch) {}
  7. private:
  8. Person(int i, char ch) :type(i), name(ch) {/*其他初始化信息*/}
  9. int type{ 1 };
  10. char name{ 'a' };
  11. };


另一种形式:

  1. // 通过委派构造函数进行优化
  2. class Person
  3. {
  4. public:
  5. Person()
  6. {
  7. InitPerson();
  8. }
  9. Person(int i) : Person()
  10. {
  11. type = i;
  12. }
  13. Person(char ch) : Person() // 委派构造函数不能使用初始化列表 初始化成员变量
  14. {
  15. name = ch;
  16. }
  17. private:
  18. void InitPerson() { /* 其他初始化 */}
  19. char name{ 'a' };
  20. };


7.5 注意事项

1.可能存在一个构造函数,它既是委托构造函数也是代理构造函数,要防止委托环的出现,否则导致构造死循环。

2.如果一个构造函数为委托构造函数,那么其初始化列表里就不能对数据成员和基类进行初始化,构造函数不能同时委派和使用初始化列表。所以如果委派构造函数要给变量赋初值,初始化代码必须放在函数体中。

8 POD 类型

8.1 背景

C++ 中最基本的存储单位是字节,C++ 中所有的数据都是由对象组成的,每一个对象都包含了一个或多个内存位置。

8.2 POD 类型

POD 是 Plain Old Data 的缩写。Plain 代表它是一个普通类型,Old 代表它是旧的,与 C 语言兼容。一个类或结构体通过 memcpy() 二进制拷贝后还能保持其数据不变,那么它就是一个POD类型。

C++11 中 POD 包含两个概念,即:平凡的和标准布局的
 

8.2.1 平凡的

一个平凡的类应符合以下定义:
①.不能定义构造/析构函数、拷贝/移动构造函数、拷贝/移动运算符,而是使用用编译器自动为我们生成的默认函数。可以使用 =default 显示的声明默认函数。
②.不能包含虚函数、虚基类

8.2.2 标准布局的

一个类是标准布局的应符合以下定义:

①.所有非静态成员有相同的访问权限(public,private,protected)
②.如果存在继承,所有非静态成员只能在其中一个类中,不可分散
③.类中第一个非静态成员的类型与其基类不同

8.3 POD 类型的应用

POD 类型的类具有以下优点:

①.字节复制我们可以很安全地使用 memset 和 memcpy 对 POD 进行初始化。

②.提供对 C 内存布局兼容

③.保证静态初始化的安全有效

对POD类型的对象来说,C++标准保证了当使用 memcpy 将对象的内容拷贝到一个 char 或者 unsigned char 的数组中再使用 memcpy 拷贝回来时,对象的内容保持不变。

  1. #include "iostream"
  2. using namespace std;
  3. class classA
  4. {
  5. public:
  6. int x;
  7. short y;
  8. };
  9. int main()
  10. {
  11. classA a;
  12. a.x = 10;
  13. a.y = 20;
  14. char * p = new char[sizeof(classA)];
  15. memcpy(p, &a, sizeof(classA));
  16. classA * a2 = reinterpret_cast<classA*>(p);
  17. cout << " memcpy 后 x: "<< a2->x << endl;
  18. cout << " memcpy 后 y: " << a2->y << endl;
  19. system("pause");
  20. return 0;
  21. }

运行结果如下:

如上图所示:正确的打印了数据,说明 memcpy 正确的拷贝了数据。


 

9 非受限联合体(union)

非受限联合体:

  • C++98中并不是所有数据类型都能够成为union的数据成员,不允许联合体拥有非POD(Plain Old Data)、静态或引用类型的成员
  • C++11中取消了联合体对于数据成员的限制,任何非引用类型都可以成为联合体的数据成员,成为非受限联合体

Example:

  1. struct Student
  2. {
  3. Student(bool g, int a): gender(g), age(a){}
  4. bool gender;
  5. int age;
  6. };
  7. union T
  8. {
  9. Student s; //C++98下编译失败,不是一个POD类型
  10. int id;
  11. char name[10];
  12. };
  13. int main()
  14. {
  15. return 0;
  16. }
  17. //编译选项:g++ -std=c++98 union.cpp
'
运行

本例中,由于Student自定义了一个构造函数,该类型是非POD的,在C++98标准中,union T无法通过编译,而在C++11中是可以的。

10 reference_wrapper

10.1 是什么?

reference_wrapper是一个模板类

template <class T> class reference_wrapper;

  • 该模板类仿真一个T类型对象的引用。但行为与普通的引用不太一样。
  • 用类型T实例化的reference_wrapper类能包装该类型的一个对象,并产生一个reference_wrapper<T>类型的对象,该对象就像是一个引用与普通引用最大的不同是:该引用可以考贝或赋值。

10.2 构造函数 和 赋值运算符

  • 没有默认构造函数
  • 可以用一个想要引用的对象构造
  • 可以用另一个reference_wrapper对象构造
  1. int a = 10;
  2. int b = 100;
  3. reference_wrapper<int> r1 = a;
  4. reference_wrapper<int> r2 = r1; //拷贝构造
  5. r1 = 1; //错误,赋值运算符只接收同类型的对象
  6. r1 = b; //正确,利用 b 构造一个临时对象,再用=运算符赋给 r1
  7. r1 = r2; //正确



10.3 get成员

通过get成员获取到reference_wrapper所引用的对象从而进行修改其值等操作访问所引用的对象的话,直接访问reference_wrapper对象即可。

  1. int a = 10;
  2. reference_wrapper<int> r1 = a;
  3. cout << r1 << endl;
  4. cout << r1.get() << endl;
  5. r1.get() = 100;
  6. cout << r1.get() << endl;


输出:

  1. 10
  2. 10
  3. 100


10.4 特殊用法

10.4.1 可以存入容器中

普通的引用不是对象,所以无法存入任何标准库容器。reference_wrapper包装的引用就可以被存入容器中。

10.4.2 可以像普通的引用一样作为实参传入函数

一个reference_wrapper<T>类型的对象,可以作为实参,传入 形参类型为T的函数中。
就像下面这样:

  1. void func1(int value) { value = 100; } 不过 value 只是个拷贝,不是引用
  2. void func2(int& value) { value = 1000; } 传入的是引用
  3. int a = 10;
  4. reference_wrapper<int> r = a;
  5. func1(r); 传入的是 r 引用的值的 拷贝
  6. cout << a << endl;
  7. func2(r); 传入的是 r 引用的值的 引用
  8. cout << a << endl;


输出:

  1. 10
  2. 1000


函数func的参数类型为int,而传递给func的参数确是reference_wrapper类型的对象。

10.4.3 其他

  • 若reference_wrapper包裹的引用是可以调用的,则reference_wrapper对象也是可调用的
  • std::ref 和std::cref 通常用来产生一个reference_wrapper对象
  • reference_wrapper 常通过引用传递对象给std::bind函数或者std::thread构造函数

11 string_view

C++17 中特别新增的一个特别好用且重要的特性,string_view相对于string来说就是一个只读的string,string_view的赋值操作的空间成本和时间成本远远胜于string,string_view的赋值特别像一个指针的赋值,一般来说在以下情况下使用string_view会更合适。

example

  1. // 常量string
  2. const std::string = "hello world";
  3. // string_view 更为合适
  4. const string_view = "hello world";
  5. // 函数参数
  6. void Function1(const std::string& arg1)
  7. {
  8. }
  9. // string_view 更为合适
  10. void Function1(string_view arg1)
  11. {
  12. }

12 optional

熟悉boost的也应该非常熟悉optional了,它最常用的地方是在你返回值是string或者int等出现错误之后非常隐式的表达的地方,使用std::optional就可以帮你解决这种问题

example:

  1. [[nodiscard]]
  2. std::optional<int> TestOptional() {
  3. // 之前我们可能需要使用return -1,代表错误,现在使用st::optional就不需要那种太过于隐式的表达
  4. if (true) {
  5. return 9999;
  6. } else {
  7. return std::nullopt;
  8. }
  9. }
  10. [[nodiscard]]
  11. std::optional<std::string> TestOptional2() {
  12. // 之前我们可能需要使用return -1,代表错误,现在使用st::optional就不需要那种太过于隐式的表达
  13. if (true) {
  14. return "helloworld";
  15. } else {
  16. return std::nullopt;
  17. }
  18. }
  19. // optional
  20. auto result = TestOptional();
  21. if (result.has_value()) {
  22. // 有值,代表成功
  23. } else {
  24. // result没有值代表失败
  25. }
  26. // 这个value_or表示当TestOptional的返回值为nullopt时使用or里面的值
  27. auto ret = TestOptional2().value_or("");

13 attribute属性说明符

属性为各种由实现定义的语言扩展(例如 GNU 与 IBM 的语言扩展 __attribute__((...)),微软的语言扩展 __declspec() 等)提供了统一化的语法。

13.1 [[fallthrough]] 标准属性

指示从前一标号直落是有意的,而在发生直落时给出警告的编译器不应诊断它。

C++17 之前的标准下, 有如下代码:

  1. switch (device.status())
  2. {
  3. case sleep:
  4. device.wake();
  5. // fall thru
  6. case ready:
  7. device.run();
  8. break;
  9. case bad:
  10. handle_error();
  11. break;
  12. }


C++17 可以这样写:

  1. switch (device.status())
  2. {
  3. case sleep:
  4. device.wake();
  5. [[fallthrough]];
  6. case ready:
  7. device.run();
  8. break;
  9. case bad:
  10. handle_error();
  11. break;
  12. }


再之前的代码编译器会告诉你没有break的警告,但是再c++17中使用fallthrough属性就可以消除这个警告了。

13.2 [[maybe_unused]] 标准属性

可用来消除未使用的函数和变量编译器所发出的警告

example:

  1. [[maybe_unused]] bool testUnusedVariable = true;
  2. [[maybe_unused]]
  3. void TestUnusedFunction() {
  4. }

13.3 [[nodiscard]] 标准属性

如果你的某个函数的返回值特别重要,希望使用者不要忽略,可以添加这个属性,再编译的时候如果函数使用者没有使用返回值将会有一个警告产生

example:

  1. [[nodiscard]] bool TestNodiscard() {
  2. return true;
  3. }

13.4 [[deprecated]] 标准属性

提示允许使用声明有此属性的名称或实体,但因为一些原因不鼓励使用,一般用在即将废弃的函数,但是还有老的用户使用到了这个函数

example:

  1. // 再vs中这个不是警告而是错误.
  2. [[deprecated("test deprecated")]] bool TestDeprecated() {
  3. return true;
  4. }

13.5 [[noretrun]] 标准属性

告知函数并没有返回值

example:

  1. [[noreturn]] void TestNoreturn() {
  2. }

14 变量模板 Variable Templates

C++14以后,变量也可以参数化为特定的类型,这称为变量模板

比如:

  1. template<typename T>
  2. constexpr T pi{3.1415926535897932385};

对于任何模板,该声明不能出现在函数内或者块区域内。

使用变量模板,必须指定它的类型:

  1. std::cout << pi<double> << '\n';
  2. std::cout << pi<float> << '\n';

可以声明在不同编译单元中使用的变量模板:

  1. // =====header.hpp
  2. template<typename T> T val{}; // zero initialized value
  3. // =====translate unit 1:
  4. #include "header.hpp"
  5. int main()
  6. {
  7. val<long> = 42;
  8. print();
  9. }
  10. // =====translate unit 2:
  11. #include "header.hpp"
  12. void print()
  13. {
  14. std::cout << val<long> << '\n'; // OK: print 42
  15. }

变量模板可以有默认模板实参:

  1. template<typename T = long double>
  2. constexpr T pi = T{3.1415926535897932385};

使用默认值或者其他类型:

  1. std::cout << pi<> << '\n'; // outputs a long double
  2. std::cout << pi<float> << '\n'; // outputs a float

但必须使用尖括号,仅仅使用pi是错误的:

std::cout << pi << '\n';  //ERROR

变量模板可以由非类型参数进行参数化,这可以用于参数化初始值:

  1. #include <iostream>
  2. #include <array>
  3. template<int N>
  4. std::array<int, N> arr{}; // array with N elements, zero initialized
  5. template <auto N>
  6. constexpr decltype(N) dval = N; // type of dval depends on passed value
  7. int main()
  8. {
  9. std::cout << dval<'c'> << '\n'; // N has value 'c' of type char
  10. arr<10>[0] = 42; // sets first element of global arr
  11. for (std::size_t i=0; i<arr<10>.size(); ++i) // uses values set in arr
  12. {
  13. std::cout << arr<10>[i] << '\n';
  14. }
  15. }

甚至当初始化和迭代arr在不同的编译单元发生时,使用的是全局范围的同一个std::array<int,10> arr变量。

数据成员的变量模板

变量模板的一个用法就是定义类模板成员的变量,比如:

  1. template<typename T>
  2. class MyClass
  3. {
  4. public:
  5. static constexpr int max = 1000;
  6. };

这允许为MyClass<>不同特化版本定义不同的值:

  1. template<typename T>
  2. int myMax = MyClass<T>::max;

因此应用开发程序员可以这么写:

auto i = myMax<std::string>;

来代替

auto i = MyClass<std::string>::max;

这意味着对于一个标准类:

  1. namespace std
  2. {
  3. template <typename T> class numeric_limits
  4. {
  5. ...
  6. static constexpr bool is_signed = false;
  7. ...
  8. };
  9. }

可以定义:

  1. template <typename T>
  2. constexpr bool isSigned = std::numeric_limits<T>::is_signed;

这样可以用isSigned<char>来代替:std::numeric_limits<char>::is_signed

类型特性后缀_v

C++17以后,标准库使用变量模板的技术为标准库中所有的类型特性定义便捷的用法,比如:

std::is_const_v<T>   // since C++17

代替

std::is_const<T>::value  // since C++11

标准库定义如下:

  1. namespace std
  2. {
  3. template<typename T> constexpr bool is_const_v = is_const<T>::value;
  4. }

15 泛型lambda

所谓泛型lambda。就是在形參声明中使用auto类型指示说明符的lambda。

比方

auto lambda = [](auto x, auto y) {return x + y;};

依据C++14标准,这一lambda与下面代码作用同样。

  1. struct unnamed_lambda
  2. {
  3. template<typename T, typename U>
  4. auto operator()(T x, U y) const {return x + y;}
  5. };
  6. auto lambda = unnamed_lambda();

C++14的泛型lambda能够被看做C++11的(单态)lambda的升级版。单态lambda相当于普通函数对象。

泛型lambda则相当于带模板參数的函数对象。或者说相当于带状态的函数模板。两者相比,能够推出下面结果:

  • 单态lambda在函数内使用,可以捕获外围变量形成闭包,作用相当于局部函数。泛型lambda强化了这一能力,其作用相当于局部函数模板。
  • 单态lambda可以服务于高阶函数(參数为函数的函数),作用相当于回调函数。泛型lambda强化了这一能力。使得泛型回调成为可能。
  • 单态lambda可以作为函数返回值,形成柯里化函数(闭包),用于lambda演算。泛型lambda强化了这一能力,使得泛型闭包成为可能。

能够说,泛型lambda大大加强了C++语言中因单态lambda的引入而有所增强的FP(函数型编程)能力。

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <algorithm>
  5. using namespace std;
  6. int main()
  7. {
  8. // 泛型局部函数
  9. auto f = [](auto x, auto y) { return x + y; };
  10. cout << f(1, 3) << endl;
  11. cout << f(string{}, "abc") << endl;
  12. // 泛型回调函数
  13. auto f2 = [](auto e) { cout << e << ","; };
  14. vector<int> v1{1, 2, 3};
  15. vector<string> v2{"a", "b", "c"};
  16. for_each(v1.begin(), v1.end(), f2); cout << endl;
  17. for_each(v2.begin(), v2.end(), f2); cout << endl;
  18. // 泛型闭包
  19. auto f3 = [](auto a) {
  20. return [=]() mutable { return a = a + a; };
  21. };
  22. auto twice1 = f3(1);
  23. cout << twice1() << endl; cout << twice1() << endl;
  24. auto twice2 = f3(string{"a"});
  25. cout << twice2() << endl; cout << twice2() << endl;
  26. }
运行结果:
/*
4
abc
1,2,3,
a,b,c,
2
4
aa
aaaa
*/

16 函数返回类型自动推导

函数返回类型自动推导是指C++11以及C++14中不直接给出函数返回类型而是使用类型指示符来指示返回类型甚至彻底省略返回类型并最终由编译器来推导返回类型的语言特性函数返回类型自动推导原则如下:

  • 当函数体内不包含任何返回值时,该函数的返回类型为void。
  • 当函数体内只包含一句带返回值的return语句时,该函数的返回类型等同于该返回值的类型。
  • 当函数体内包含多个带返回值的return语句时,该函数的返回类型由这些返回值的类型共同决定,所有返回值的类型必须相同。
  • 函数体内的返回值可以递归调用自身,但这类无法由编译器自动推导类型的返回值,必须发生在可以由编译器自动推导类型的非递归调用的返回值之后。

在lambda表达式中省略返回类型
当lambda表达式中省略返回类型时,lambda表达式的返回类型由编译器根据返回值以及模板参数推导规则进行自动推导。

声明函数时使用auto指示符来指示返回类型
当函数的返回类型包含auto指示符并且函数的后置返回类型被省略时,函数的返回类型由编译器根据返回值以及模板参数推导规则进行自动推导。

声明函数时使用decltype(auto)指示符来指示返回类型
当函数的返回类型为decltype(auto)指示符时,函数的返回类型由编译器根据返回值以及decltype推导规则进行自动推导。

  1. #include <iostream>
  2. #include <string>
  3. #include <typeinfo>
  4. template<typename T>
  5. T f();
  6. auto f2(int n)
  7. {
  8. return n + 1;
  9. }
  10. auto f3(bool b)
  11. {
  12. if(b) return 1.0;
  13. else return 2.0;
  14. }
  15. auto f4(int n)
  16. {
  17. if(n == 0) return 1;
  18. else return n * f4(n - 1);
  19. }
  20. struct S
  21. {
  22. int n1, n2;
  23. decltype(auto) f6(bool b)
  24. {
  25. if(b) return (n1);
  26. else return (n2);
  27. }
  28. };
  29. int main()
  30. {
  31. auto f1 = [](int a){std::cout << a;};
  32. auto f5 = [](auto a, auto b){return a + b;};
  33. f<decltype(f1(1))>();
  34. f<decltype(f2(1))>();
  35. f<decltype(f3(false))>();
  36. f<decltype(f4(3))>();
  37. f<decltype(f5(1.0f, 3.0f))>();
  38. S s;
  39. f<decltype(s.f6(false))>();
  40. }

  1. main.cpp:(.text.startup+0x5): undefined reference to `void f<void>()'
  2. main.cpp:(.text.startup+0xa): undefined reference to `int f<int>()'
  3. main.cpp:(.text.startup+0xf): undefined reference to `double f<double>()'
  4. main.cpp:(.text.startup+0x14): undefined reference to `int f<int>()'
  5. main.cpp:(.text.startup+0x19): undefined reference to `float f<float>()'
  6. main.cpp:(.text.startup+0x1e): undefined reference to `int& f<int&>()'


这段代码使用了一种特殊技巧来查看编译器所实际推导出的函数返回类型。代码中定义了一个未被实现的函数模板f,该函数模板有一个类型参数T。如果用某个类型T来调用该函数模板,编译器就会因找不到函数模板的定义而输出”T f<T>()未被定义“的出错信息。也就是说,利用该函数模板,我们便可以在编译器所输出的出错信息中检查模板参数T的实际类型。在main函数内,我们充分利用了这一个技巧,通过利用函数返回类型调用该函数模板来让编译器输出相应的类型。
从出错信息中可以得知:

  • lambda表达式f1的返回类型被推导为void。
    • 理由:lambda表达式的函数体内没有任何返回值。
  • 函数f2的返回类型被推导为int。
    • 理由:函数体内只包含一个return语句,返回值n+1类型为int。
  • 函数f3的返回类型被推导为double。
    • 理由:函数体内包含两个return语句,返回值1.0以及2.0类型都是double。
  • 函数f4的返回类型被推导为int。
    • 理由:函数体内包含两个return语句,其中第二个返回值递归调用自身,无法推导,而第一个返回值1类型为int。
  • lambda表达式f5的返回类型被推导为表达式a+b的类型。当 a=1.0f, b=3.0f 时,f5的返回类型为表达式 1.0f + 3.0f 的类型,即float。
    • 理由:函数体内只包含一个return语句,返回值a+b类型取决于泛型参数a以及b的类型。
  • 函数f6的返回类型被推导为int&。
    • 理由:函数体内包含两个return语句,将返回值(n1)以及(n2)代入decltype(auto)结果类型都是int&。
       

17 函数引用后置修饰符

&

C++11引入的功能,左值引用限定符指示函数只能被左值对象调用

&&

C++11引入,右值引用限定符指示函数只能被右值调用如果函数没有引用限定符修饰,左值和右值均可调用

一个引用限定例子:

18 C++函数返回值类型后置(跟踪返回值类型)

在泛型编程中,可能需要通过参数的运算来得到返回值的类型。考虑下面这个场景:

  1. template <typename R, typename T, typename U>
  2. R add(T t, U u)
  3. {
  4. return t+u;
  5. }
  6. int a = 1; float b = 2.0;
  7. auto c = add<decltype(a + b)>(a, b);

我们并不关心 a+b 的类型是什么,因此,只需要通过 decltype(a+b) 直接得到返回值类型即可。但是像上面这样使用十分不方便,因为外部其实并不知道参数之间应该如何运算,只有 add 函数才知道返回值应当如何推导。

那么,在 add 函数的定义上能不能直接通过 decltype 拿到返回值呢?

  1. template <typename T, typename U>
  2. decltype(t + u) add(T t, U u) // error: t、u尚未定义
  3. {
  4. return t + u;
  5. }

当然,直接像上面这样写是编译不过的。因为 t、u 在参数列表中,而 C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在。

可行的写法如下

  1. template <typename T, typename U>
  2. decltype(T() + U()) add(T t, U u)
  3. {
  4. return t + u;
  5. }

考虑到 T、U 可能是没有无参构造函数的类,正确的写法应该是这样:

虽然成功地使用 decltype 完成了返回值的推导,但写法过于晦涩,会大大增加 decltype 在返回值类型推导上的使用难度并降低代码的可读性。

因此,在 C++11 中增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。

返回类型后置语法是通过 auto 和 decltype 结合起来使用的。上面的 add 函数,使用新的语法可以写成:

  1. template <typename T, typename U>
  2. auto add(T t, U u) -> decltype(t + u)
  3. {
  4. return t + u;
  5. }

为了进一步说明这个语法,再看另一个例子:

  1. int& foo(int& i);
  2. float foo(float& f);
  3. template <typename T>
  4. auto func(T& val) -> decltype(foo(val))
  5. {
  6. return foo(val);
  7. }

如果说前一个例子中的 add 使用 C++98/03 的返回值写法还勉强可以完成,那么这个例子对于 C++ 而言就是不可能完成的任务了。

在这个例子中,使用 decltype 结合返回值后置语法很容易推导出了 foo(val) 可能出现的返回值类型,并将其用到了 func 上。

返回值类型后置语法,是为了解决函数返回值类型依赖于参数而导致难以确定返回值类型的问题。有了这种语法以后,对返回值类型的推导就可以用清晰的方式(直接通过参数做运算)描述出来,而不需要像 C++98/03 那样使用晦涩难懂的写法。

19 constexpr

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

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

19.1 constexpr修饰普通变量:

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

值得一提的是,使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式。举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. constexpr int num = 1 + 2 + 3;
  6. int url[num] = {1,2,3,4,5,6};
  7. couts<< url[1] << endl;
  8. return 0;
  9. }

程序执行结果为:

2

读者可尝试将 constexpr 删除,此时编译器会提示“url[num] 定义中 num 不可用作常量”。

可以看到,程序第 6 行使用 constexpr 修饰 num 变量,同时将 "1+2+3" 这个常量表达式赋值给 num。由此,编译器就可以在编译时期对 num 这个表达式进行计算,因为 num 可以作为定义数组时的长度。

有读者可能发现,将此示例程序中的 constexpr 用 const 关键字替换也可以正常执行,这是因为 num 的定义同时满足“num 是 const 常量且使用常量表达式为其初始化”这 2 个条件,由此编译器会认定 num 是一个常量表达式。

注意,const 和 constexpr 并不相同,关于它们的区别,我们会在下一节做详细讲解。

另外需要重点提出的是,当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。

19.2 constexpr修饰函数:

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

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

1) 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。

举个例子:

  1. constexpr int display(int x) {
  2. int ret = 1 + 2 + x;
  3. return ret;
  4. }

注意,这个函数是无法通过编译的,因为该函数的返回值用 constexpr 修饰,但函数内部包含多条语句。

如下是正确的定义 display() 常量表达式函数的写法:

  1. constexpr int display(int x) {
  2. //可以添加 using 执行、typedef 语句以及 static_assert 断言
  3. return 1 + 2 + x;
  4. }

可以看到,display() 函数的返回值是用 constexpr 修饰的 int 类型值,且该函数的函数体中只包含一个 return 语句。



2) 该函数必须有返回值,即函数的返回值类型不能是 void。

举个例子:

  1. constexpr void display() {
  2. //函数体
  3. }

像上面这样定义的返回值类型为 void 的函数,不属于常量表达式函数。原因很简单,因为通过类似的函数根本无法获得一个常量。

3) 函数在使用之前,必须有对应的定义语句。我们知道,函数的使用分为“声明”和“定义”两部分,普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。

举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. //普通函数的声明
  4. int noconst_dis(int x);
  5. //常量表达式函数的声明
  6. constexpr int display(int x);
  7. //常量表达式函数的定义
  8. constexpr int display(int x){
  9. return 1 + 2 + x;
  10. }
  11. int main()
  12. {
  13. //调用常量表达式函数
  14. int a[display(3)] = { 1,2,3,4 };
  15. cout << a[2] << endl;
  16. //调用普通函数
  17. cout << noconst_dis(3) << endl;
  18. return 0;
  19. }
  20. //普通函数的定义
  21. int noconst_dis(int x) {
  22. return 1 + 2 + x;
  23. }

程序执行结果为:

3
6

读者可自行将 display() 常量表达式函数的定义调整到 main() 函数之后,查看编译器的报错信息。

可以看到,普通函数在调用时,只需要保证调用位置之前有相应的声明即可;而常量表达式函数则不同,调用位置之前必须要有该函数的定义,否则会导致程序编译失败。

4) return 返回的表达式必须是常量表达式,举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. int num = 3;
  4. constexpr int display(int x){
  5. return num + x;
  6. }
  7. int main()
  8. {
  9. //调用常量表达式函数
  10. int a[display(3)] = { 1,2,3,4 };
  11. return 0;
  12. }

该程序无法通过编译,编译器报“display(3) 的结果不是常量”的异常。

常量表达式函数的返回值必须是常量表达式的原因很简单,如果想在程序编译阶段获得某个函数返回的常量,则该函数的 return 语句中就不能包含程序运行阶段才能确定值的变量。

注意,在常量表达式函数的 return 语句中,不能包含赋值的操作(例如 return x=1 在常量表达式函数中不允许的)。另外,用 constexpr 修改函数时,函数本身也是支持递归的,感兴趣的读者可自行尝试编码测试。

19.3 constexpr修饰类的构造函数:

对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。

举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. //自定义类型的定义
  4. constexpr struct myType {
  5. const char* name;
  6. int age;
  7. //其它结构体成员
  8. };
  9. int main()
  10. {
  11. constexpr struct myType mt { "zhangsan", 10 };
  12. cout << mt.name << " " << mt.age << endl;
  13. return 0;
  14. }

此程序是无法通过编译的,编译器会抛出“constexpr不能修饰自定义类型”的异常。

当我们想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数。例如,修改上面的错误示例如下:

  1. #include <iostream>
  2. using namespace std;
  3. //自定义类型的定义
  4. struct myType {
  5. constexpr myType(char *name,int age):name(name),age(age){};
  6. const char* name;
  7. int age;
  8. //其它结构体成员
  9. };
  10. int main()
  11. {
  12. constexpr struct myType mt { "zhangsan", 10 };
  13. cout << mt.name << " " << mt.age << endl;
  14. return 0;
  15. }

程序执行结果为:

zhangsan 10

可以看到,在 myType 结构体中自定义有一个构造函数,借助此函数,用 constexpr 修饰的 myType 类型的 my 常量即可通过编译。

注意,constexpr 修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式。

前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4 个条件。

举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. //自定义类型的定义
  4. class myType {
  5. public:
  6. constexpr myType(const char *name,int age):name(name),age(age){};
  7. constexpr const char * getname(){
  8. return name;
  9. }
  10. constexpr int getage(){
  11. return age;
  12. }
  13. private:
  14. const char* name;
  15. int age;
  16. //其它结构体成员
  17. };
  18. int main()
  19. {
  20. constexpr struct myType mt { "zhangsan", 10 };
  21. constexpr const char * name = mt.getname();
  22. constexpr int age = mt.getage();
  23. cout << name << " " << age << endl;
  24. return 0;
  25. }

程序执行结果为:

zhangsan 10

注意,C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法。

19.4 constexpr修饰模板函数

C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。

针对这种情况下,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。

举个例子:

  1. #include <iostream>
  2. using namespace std;
  3. //自定义类型的定义
  4. struct myType {
  5. const char* name;
  6. int age;
  7. //其它结构体成员
  8. };
  9. //模板函数
  10. template<typename T>
  11. constexpr T dispaly(T t){
  12. return t;
  13. }
  14. int main()
  15. {
  16. struct myType stu{"zhangsan",10};
  17. //普通函数
  18. struct myType ret = dispaly(stu);
  19. cout << ret.name << " " << ret.age << endl;
  20. //常量表达式函数
  21. constexpr int ret1 = dispaly(10);
  22. cout << ret1 << endl;
  23. return 0;
  24. }

程序执行结果为:

zhangsan 10
10

可以看到,示例程序中定义了一个模板函数 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

  • 第 20 行代码处,当模板函数中以自定义结构体 myType 类型进行实例化时,由于该结构体中没有定义常量表达式构造函数,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;
  • 第 23 行代码处,模板函数的类型 T 为 int 类型,实例化后的函数符合常量表达式函数的要求,所以该函数的返回值就是一个常量表达式。

20 Constexpr if

在 constexpr if 语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。

通常我们写业务很难用到,在模板元编程中会特别有用。

example:

  1. template <typename T>
  2. void TestConstexprIf(T value) {
  3.     if constexpr (std::is_integral_v<T>)
  4.         std::cout << "is integral" << std::endl;
  5.     else
  6.         static_assert(false, "T必须是整型");
  7. }

21 嵌套命名空间

example

  1. namespace test::test2 {
  2. int i = 0;
  3. }
  4. std::cout << test::test2::i << std::endl;

22 filesystem

在没有C++17时一直就使用experiment/filesystem,在C++17 filesystem被正式纳入C++标准库中, 由于大多数人对filesystem都比较熟悉了,在这里只是简单的介绍一下

example:

  1. std::filesystem::path path("testpath");
  2. if (std::filesystem::exists(path)) {
  3. // 存在
  4. } else {
  5. // 不存在
  6. }

24 any

any是一个可用于任何类型单个值的类型安全容器,如果你之前有了解过boost相信对这个any类已经非常熟悉了

example:

  1. std::any Int = 69;
  2. std::any Double = 69.123;
  3. std::any String = std::string_view("Hello");
  4. std::cout << Int.type().name() << std::endl;
  5. std::cout << Double.type().name() << std::endl;
  6. std::cout << Double.type().name() << std::endl;
  7. std::vector<std::any> anys = { Int, Double, String };
  8. std::cout << std::any_cast<int>(Int) << std::endl;
  9. std::cout << std::any_cast<double>(Double) << std::endl;
  10. std::cout << std::any_cast<std::string_view>(String) << std::endl;
  11. // has_value: 是否有值
  12. std::any a = 1;
  13. if (a.has_value()) {
  14. std::cout << a.type().name() << std::endl;// i
  15. }
  16. // reset:清空容器
  17. a.reset();
  18. if (a.has_value()) {
  19. std::cout << "no value\n";// no value
  20. }

25 variant

  • variant用来表示一个类型安全的联合体,variant 的一个实例在任意时刻要么保有其一个可选类型之一的值,要么在错误情况下无值。
  • variant 不容许保有引用、数组,或类型 void, 空variant可以使用std::variant<std::monostate>


下面这个例子是一个实际接口设计时利用std::variant解决不同类型不同参数的接口。

  1. // variant
  2. struct SystemProxyConfig {
  3. bool isService;
  4. };
  5. struct CustomProxyConfig {
  6. bool isFile;
  7. std::string pathOrContent;
  8. };
  9. std::variant<SystemProxyConfig, CustomProxyConfig> config;
  10. //config = CustomProxyConfig{ false, "http://192.168.21.161/spiderweb.pac" };
  11. config = SystemProxyConfig{ false };
  12. if (std::get_if<CustomProxyConfig>(&config)) {
  13. // 类型成功
  14. CustomProxyConfig customConfig = std::get<CustomProxyConfig>(config);
  15. } else {
  16. // 类型失败
  17. SystemProxyConfig systemProxyConfig = std::get<SystemProxyConfig>(config);
  18. int i = 0;
  19. }


 

26 execution

execution为C++STL算法库提供了一种算法的执行策略设置,目前支持的策略:

  • sequenced_policy (顺序执行策略)
  • parallel_policy (并行执行策略)
  • parallel_unsequenced_policy (并行及无序执行策略)
  • unsequenced_policy (无序执行策略)

example:

  1. std::vector<int> testExecution{ 1, 2, 3, 4, 5,8, 19, 20 ,30,40,50,0,102,40,10,30,20,1000,32,31,34,45};
  2. auto it1 = std::find(std::execution::seq, testExecution.begin(), testExecution.end(), 5);
  3. auto it2 = std::find(std::execution::par, testExecution.begin(), testExecution.end(), 5);
  4. auto it3 = std::find(std::execution::par_unseq, testExecution.begin(), testExecution.end(), 5);

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/1009044
推荐阅读
相关标签
  

闽ICP备14008679号