文章目录
- 前言
- 前奏
- lambda
- 浅谈std::ref的实现
- 浅谈is_same
- 浅谈std::function的实现
- std::visit 与 std::variant 与运行时多态
- SFINAE
- 类型内省
- 标签分发 (tag dispatching)
- 编译时多态
- 奇异递归模板模式 (Curiously Recurring Template Pattern,CRTP)
- 三路比较操作符 (飞船操作符) <=>
- enable_shared_from_this 模板类
- 概念约束
- 定义概念
- requires 表达式
- 模板元编程
- constexpr 元编程
- Ranges 标准库
- 协程
- 软件设计六大原则 SOLID
- To be continue....
前言
- C++20 是C++在C++11 之后最大的一次语言变革, 其中引入了大量具有革命性的新特性.
- 本节包含了C++20中相当重要的四大特性: 概念约束, ranges(范围)标准库, 协程以及模块
概念约束
:
是一个编译期谓词, 它根据程序员定义的接口规范对类型,常量等进行编译时检查,以便在泛型编程中为使用者提供更好的可读性与错误信息.ranges标准库
:
对现有的标准库进行了补充,它以函数式编程范式进行编程, 将计算任务分解成一系列灵活的原子操作, 使得代码的正确性更容易推理.协程
:
是一种可挂起, 可恢复的通用函数, 它的切换开销是纳秒级的, 相对其他方案而言占用的资源极低, 并且可以非侵入式地为已有库扩展协程接口, 它常常用于并发编程, 生成器, 流处理, 异常处理等.模块
:
解决了传统的头文件编译模型的痛点: 依赖顺序导致头文件难以组合, 重复解析, 符号覆盖等问题, 从语言层面为程序员提供了模块化的手段.
- 涉及了一些元编程的概念
前奏
lambda
- lambda其实是由编译器生成的一个匿名类
- 如果lambda包含在捕获列表内, 那么捕获将在对应的匿名类中生成成员变量与构造函数来存储捕获;
- 对于无捕获的lambda而言, 其生成的匿名类中拥有一个非虚的函数指针类型转换操作符, 能够将lambda转换成函数指针. 这个不难理解, 因为无状态的 lambda 表达式可以赋给无状态的函数指针;
- 在C++20 中泛型 lambda 也支持以模板参数形式提供, 这样就能保证两个形参类型一致.
auto add=[]<typename T>(T a,T b){return a+b;};
cout<<add(1,2)<<endl;
- 模板函数只有实例化之后才能传递, 而泛型 lambda 是一个对象,可以按值传递, 在调用时根据实际传参进行实例化 模板函数 operator(),从而延迟了实例化的时机, 大大提高了灵活性. 标准库的一些算法通常要求对函数对象进行组合, 此时泛型 lambda 将能通过编译, 而模板函数不行.
浅谈std::ref的实现
- 就是说构造一个新的对象(reference_wrapper是一个类模板)并在内部保存原来传入的变量的地址 _f 和类型 type
- 重载类型转换为原来变量的引用类型, 有了_f 和 type, 这样可以在需要的时候自动将reference_wrapper转换为原来传入的变量实体的引用;
- reference_wrapper 她本身可能会被 decay 但内部存储的 _f 值始终不会变, 始终可以在需要的时候自动转换为 type&
- 我们可以自己简单实现一下:
template<typename T> class my_ref{ public:T * _f;explicit my_ref(T& var): _f(addressof(var)){};operator T& () { return *_f; }; //如果传入的是一个可调用对象template<class... Args>auto&& operator()(Args&& ...args){return (*_f)(args...);} };
- 参考: https://zhuanlan.zhihu.com/p/581739392
浅谈is_same
- 这是C++11引入的, 其实很简单, 模板特化就行了. 让编译器决定. 考虑: 万一我要运行时才能确定类型呢?
template<class T, class U> struct is_same : std::false_type {}; //偏特化版本 待确认一个模板参数 template<class T> struct is_same<T, T> : std::true_type {};
浅谈std::function的实现
- 有点类似上面的std::ref
- 主要是对函数参数列表类型的获取, 可以使用模板特化来达到目的 C++ Template -> [5] -> 自由函数与模板可变参数
std::visit 与 std::variant 与运行时多态
- 用法参考:
https://zhuanlan.zhihu.com/p/676918348
https://zhuanlan.zhihu.com/p/670189611 - std::variant 行为像是一个类型安全的联合体。它存储了一系列类型,并能够在运行时安全地处理这些类型之一。
- 它保留足够的空间来存储其可能的任何类型,通常是这些类型中最大者的大小。此外,std::variant 还需要额外的存储空间来跟踪当前存储的类型。
- 为了维护类型安全,std::variant 使用一个内部索引来标记当前激活的类型。当访问或修改 std::variant 的值时,它会检查这个索引,并确保操作符合当前激活的类型。
- std::visit 的第一个参数传入一个可调用对象,后面传入的是可调用对象的参数(可以用variant)。 std::visit 的工作原理依赖于编程语言或编译器的内部机制,这些机制通常对程序员透明。其中一种可能的实现方式是使用 “vtable”(Virtual Table,虚函数表)。 总之, std::visit 会在运行时查找函数地址。每当创建一个 std::variant 对象的时候,就会产生一个与之关联的 vtable,同来存储这个 std::variant 中存储的相关信息。
- visit 编译时静态绑定 (第二个参数中各类型对应的第一个参数中可调用函数版本绑定到类型对应的索引 (用vtable来存储映射关系) )
- visit 运行时动态绑定 (检查variant中当前激活的类型的索引). 从而决定要调用的函数版本
- subtype多态例子:
#include <iostream> #include <memory> #include <cmath> namespace Subtype {struct Shape{virtual ~Shape() = default;virtual double getArea() const = 0;virtual double getPerimeter() const = 0;};struct Circle: Shape{Circle(double r): r_(r) {}double getArea() const override{return M_PI * r_ * r_;}double getPerimeter() const override{return 2 * M_PI * r_;}private:double r_;};struct Rectangle: Shape{Rectangle(double w, double h): w_(w), h_(h) {}double getArea() const override{return w_ * h_;}double getPerimeter() const override{return 2 * (w_ + h_);}private:double w_;double h_;}; } using namespace Subtype;int main(int argc, char** argv) {std::unique_ptr<Shape> shape = std::make_unique<Circle>(2);// shape area: 12.5664 perimeter: 12.5664std::cout << "shape area: " << shape->getArea()<< " perimeter: " << shape->getPerimeter() << std::endl;shape = std::make_unique<Rectangle>(2, 3);// shape area: 6 perimeter: 10std::cout << "shape area: " << shape->getArea()<< " perimeter: " << shape->getPerimeter() << std::endl;return 0; }
- ad-hoc多态例子:
#include <variant> #include <cmath> #include <iostream> namespace Adhoc {struct Circle{double r;};// Circle的一系列操作double getArea(const Circle& c){return M_PI * c.r * c.r;}double getPerimeter(const Circle& c){return 2 * M_PI * c.r;};struct Rectangle{double w;double h;};// Rectangle的一系列操作double getArea(const Rectangle& r){return r.w * r.h;}double getPerimeter(const Rectangle& r){return 2 * (r.w + r.h);};// 通过加法类型定义一个统一的类型Shape,其拥有不同的形状,从而实现运行时多态using Shape = std::variant<Circle, Rectangle>;// 统一类型Shape的一系列多态行为double getArea(const Shape& s){return std::visit([](const auto & data){return getArea(data);}, s);}double getPerimeter(const Shape& s){return std::visit([](const auto & data){return getPerimeter(data);}, s);}; } using namespace Adhoc;int main(int argc, char** argv) {Shape shape = Circle{2};// shape area: 12.5664 perimeter: 12.5664std::cout << "shape area: " << getArea(shape)<< " perimeter: " << getPerimeter(shape) << std::endl;shape = Rectangle{2, 3};// shape area: 6 perimeter: 10std::cout << "shape area: " << getArea(shape)<< " perimeter: " << getPerimeter(shape) << std::endl;return 0; }
subtype 多态和 ad-hoc 多态的表现形式对比
多态形式 定义 多态调用 subtype 多态 Abstract* obj obj->method() ad-hoc 多态 Abstract obj method(obj)
SFINAE
-
substitution failure is not an error
-
典型的用法是利用enable_if
enable_if:template<bool, typename _Tp = void>struct enable_if{ };// Partial specialization for true. template<typename _Tp>struct enable_if<true, _Tp>//第二个参数默认为void{ typedef _Tp type; };
如果是false, 那么第一个空类没有type成员, 这时直接使用其type将报错
但在SFINAE决策上下文环境中, 这种情况可以做为一种决策条件:下面情况下, 编译器如果根据实参推断发现enable_if<false>没有定义成员类型type, 将导致替换失败, 将其从候选集中删除, 从而达到我们的目的
template<class T, enable_if_t<is_integral_v<T>>* = nullptr> void test(T t) {cout << "is integral" << endl;};template<class T, enable_if_t<is_floating_point_v<T>>* = nullptr> void test(T t) {cout << "is floating_point" << endl;};
-
Tips: 函数重载的过程中只看函数的声明, 如果它被决策为最佳可行函数, 但模板函数体内发生了模板参数替换失败, 那么就会在实例化过程中产生编译错误, 而不是SFINAE
-
Tips: SFINAE机制在其他条件相同情况下, 总是会优先选择更明确更具体的类型版本, 这也是模板特化能起作用的原因
类型内省
- 检查对象的类型或属性的一种能力
- 在C++中类型萃取也可以视作内省
- 就能实现把各种类型各种分离与组合, <type_trait> 实现了这些功能
- 比如 C++ Template -> [5] -> 自由函数与模板可变参数 中也是利用了类型内省 类似的还有数组
template<class E,size_t N> someArr<E[N]>{…}
标签分发 (tag dispatching)
-
除了 enable_if 之外的编译时多态手段还有标签分发, 这也是C++社区著名的管用手法;
-
标签常常是一个空类, 没有别的什么, 只是当作一种类型. 好让编译器在SFINAE决策中把它作为参考依据, 匹配出最合适的版本
-
辅助类 true_type 和 false_type 类型也可视作标签, 他们把 true 和 false 包装成两种类型 这样在编译时就能决策出最合适的版本
-
示例:
template<class T>bool numEqImpl(T l, T r, true_type) {cout << "is floating" << endl;return true; } template<class T>bool numEqImpl(T l, T r, false_type) {cout << "is not floating" << endl;return false; } template<class T> enable_if_t<is_arithmetic_v<T>, bool> numEq(T l, T r) {return numEqImpl(l, r, is_floating_point<T> {});//标签分发 }
怎么样, 是不是逐渐理解一切了?
标准库中在<iterator>中定义了如下迭代器标签
input_iterator_tag;
forward_iterator_tag;
bidirectional_iterator_tag;
random_access_iterator_tag;
并且提供了配套的元函数 iterator_traits, 输入迭代器,输出相关属性;
例如类型成员::iterator_category 存储的是迭代器的种类标签,::value_type 是解引用后的类型, ::difference_type 为迭代器作差后的类型 ptrdiff_t(通常是long类型的别名)iterator_traits<myIterator>::iterator_category{}
实际调用时这样开始分发 ,有了标签就不用浪费内存真的去构造一个实体iterator对象了(大部分标签实现为空类)
看看advance
函数 它也利用了迭代器标签来实现也可以尝试利用 if constexpr 来实现
advance
编译时多态
除了前面多多少少讲的一些编译时多态手段, 还有下面这些
-
奇异递归模板模式 (Curiously Recurring Template Pattern,CRTP)
- 代码复用: 由于子类派生于模板基类, 因此可以复用基类的方法
- 编译时多态: 由于基类是一个模板类, 能够获得传递进来的派生类, 进而可以调用派生类的方法, 达到多态的效果. 与运行时多态相比没有虚表的开销.
- 通常是如下形式:
template<class T> struct Base{/*...*/};//派生类struct Derived : Base<Derived>{/*...*/};
看一个利用 CRTP 形成静态多态的例子:
#include <iostream>using namespace std;template<typename Derived>struct Animal {void bark() { static_cast<Derived&>(*this).barkImpl(); }};class Cat: public Animal<Cat> {friend Animal;void barkImpl() { cout << "Miaowing!" << endl; }};class Dog: public Animal<Dog> {friend Animal;void barkImpl() { cout << "Bow wow!" << endl; }};template<typename T>void play(Animal<T>& animal) { animal.bark(); }int main(int argc, char** argv) {Cat cat; play(cat); // Miaowing!Dog dog; play(dog); // Bow wow!return 0;}
从play的实现来看, 它接受一个统一的抽象接口类, 而调用的时候根据实际类型是Cat还是Dog进行多态调用, 由于类型是在编译时确定的, 所以多态调用也是在编译时绑定的. 为何不把play写成模板函数的形式? 这样无需模板基类Animal<T>也能实现静态多态.
原因在于CRTP的模板基类可以起到接口类的作用, 它明确列出了该派生体系中所支持的一组操作, 换句话说Animal<T>其实是对它所接受的类型,所支持的方法的集合进行约束, 而普通的模板函数对模板参数没有约束, 需要使用高级技巧(如 enable_if)进行约束.
C++演进的过程中考虑了这一点, 在 C++20 中提供 concept 特性对模板参数进行约束.Visitor Pattern (访问者模式)
点击查看代码
虽然现代C++使用 std::variant 与 std::visit 来代替访问者模式
但先辈们想出来的这个复合了奇异递归模板模式的模式…其思想…高内聚低耦合总是现代C++所追求的…#include <iostream> #include <string> #include <memory> #include <vector> #include <variant>using namespace std;struct VideoFile; struct TextFile;struct Visitor {virtual void visit(VideoFile&) = 0;virtual void visit(TextFile&) = 0;virtual ~Visitor() = default; };struct Elem {virtual void accept(Visitor& visit) = 0;virtual ~Elem() = default; };template<typename T> struct dump;template<typename Derived> struct AutoDispatchElem: Elem {void accept(Visitor& visitor) override{ visitor.visit(static_cast<Derived&>(*this)); } };struct VideoFile: AutoDispatchElem<VideoFile> { /* ... */ }; struct TextFile: AutoDispatchElem<TextFile> { /* ... */ };int main(int argc, char** argv) {std::vector<std::unique_ptr<Elem>> elems;elems.emplace_back(new VideoFile);elems.emplace_back(new TextFile);struct ConcreteVisitor: Visitor {void visit(VideoFile&) override{ cout << "process VideoFile ..." << endl; }void visit(TextFile&) override{ cout << "process TextFile ..." << endl; }} visitor;for (auto&& e: elems) e->accept(visitor);return 0; }
三路比较操作符 (飞船操作符) <=>
-
C++20 引入了三路比较操作符, 这样只需要实现一个直观的<=>操作符,就可以直接得到六个比较操作符<,==,!=,>,<=,>=
点击查看代码
#include <iostream> #include <tuple> #include <cassert>using namespace std;namespace crtp { template<typename Derived> struct Comparable {friend bool operator==(const Derived& lhs, const Derived& rhs){ return lhs.tie() == rhs.tie(); }friend bool operator!=(const Derived& lhs, const Derived& rhs){ return !(lhs == rhs); }friend bool operator< (const Derived& lhs, const Derived& rhs){ return lhs.tie() < rhs.tie(); }friend bool operator> (const Derived& lhs, const Derived& rhs) { return rhs < lhs; }friend bool operator<=(const Derived& lhs, const Derived& rhs) { return !(lhs > rhs); }friend bool operator>=(const Derived& lhs, const Derived& rhs) { return !(lhs < rhs); } };struct Point: Comparable<Point> {Point(int x, int y): x(x), y(y) { }int x; int y;auto tie() const { return std::tie(x, y); } }; }namespace classical { struct Point {int x; int y;friend bool operator==(const Point& lhs, const Point& rhs){ return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); }friend bool operator<(const Point& lhs, const Point& rhs){ return std::tie(lhs.x, lhs.y) < std::tie(rhs.x, rhs.y); }friend bool operator!=(const Point& lhs, const Point& rhs) { return !(lhs == rhs); }friend bool operator> (const Point& lhs, const Point& rhs) { return rhs < lhs; }friend bool operator<=(const Point& lhs, const Point& rhs) { return !(lhs > rhs); }friend bool operator>=(const Point& lhs, const Point& rhs) { return !(lhs < rhs); } }; }namespace modern { struct Point { // C++20int x; int y;friend auto operator<=>(const Point& lhs, const Point& rhs) = default; }; }#define PointTest() assert((Point{1,2} >= Point{1,0})); \assert((Point{1,2} <= Point{5,0})); \assert((Point{1,2} == Point{1,2})); \assert((Point{1,2} != Point{1,3})); \assert((Point{1,2} < Point{1,3})); \assert((Point{3,2} > Point{1,3}))int main(int argc, char** argv) {{using crtp::Point;PointTest();}{using classical::Point;PointTest();}{using modern::Point;PointTest();}return 0; }
enable_shared_from_this 模板类
- 异步编程中存在一种场景, 需要在类中将该类的对象注册到某个回调类或函数中, 不能简单的将this传到回调类中, 很可能因为回调时该对象被释放而导致野指针访问, 也不能直接将this构造成shared_ptr, 因为无法直接通过裸指针获得引用计数信息, 强行构造也会导致对象内存被多次释放.
那么如果获得裸指针(this) 的引用计数信息呢? 标准库提供了enable_shared_from_this 模板基类 通过CRTP在父类中存储子类的指针信息与引用计数信息, 并提供 shared_from_this 和 weak_from_this 以获得智能指针.struct Obj {void submit() {executor.submit([this] { this->onComplete(); });//内存风险}void onComplete() {/*...*/} };
为在类内处理它本身引用计数逻辑提供了便利struct Obj : enable_shared_from_this<Obj> {void submit() {executor.submit([obj = weak_from_this()] {if (auto spObj = obj.lock()) spObj->onComplete();});}void onComplete() {/*...*/} };
概念约束
- C++20 起对 concept 特性进行了标准化, 主流的编译器也提供了支持
将一类数据类型和对它的一组操作所满足的公理集称为 concept: 不仅需要从语法上满足要求, 还需要从语义层面上满足. - concept 拥有强大的表达力, 程序员能够非常简单的定义一个概念, 也可以对概念库中已有的概念进行组合. 概念支持重载, 能够消除对变通方案(enable_if等技巧)的依赖, 大大降低了元编程的难度, 也简化了泛型编程.
定义概念
-
是一个对类型约束的编译期谓词, 给定一个类型判断其能否满足语法和语义要求, 这对泛型编程而言极为重要.比如, 给定模板参数T, 对它的要求如下:
- 一种迭代器类型 Iterator<T>
- 一种数字类型 Number<T>
Iterator 和 Number 就是概念
-
基本定义语法:
template<被约束的模板参数列表>
concept 概念名 = 约束表达式;概念和模板 using 的别名很类似, 前者是对布尔表达式的别名, 而后者是对模板类型的别名.
约束表达式 可以简单理解为 布尔常量表达式
如果在定义概念时约束表达式类型不为 bool 类型, 将引发一个编译错误, 而不是返回 false//Atomic constraint must be of type 'bool' (found 'int') template <typename T> concept Foo=1;
>.GCC编译器定义concept时不会报错, 在求值的时候才进行类型检查;
-
在概念标准库<concepts>中可以看到:
template<typename T> concept integral = is_integral_v<T>; template<typename T> concept floating_point = is_floating_point_v<T>;
简单使用展示:
static_assert(floating_point<float>); static_assert(! floating_point<int>);
-
约束表达式中使用 && 与 ||
合取(conjunction)与析取(disjunction)操作与逻辑表达式中的与或运算类似, 也是一个短路操作符.template <typename T> concept Foo = is_integral_v<typename T::type>||sizeof(T)>1; static_assert(Foo<double>);//ok,因为 sizeof(T)>1 满足(true)
-
原子约束
在依次对每个条件检查时, 首先检查表达式是否合法, 若不合法则该约束不满足,返回false 否则进一步对约束进行求值判断是否满足.
对可变模板参数形成的约束表达式, 既不是约束合取也不是约束析取//要求每个模板参数都含有类型成员type, 否则整个表达式为false, //因为原子约束先检查整个表达式是否合法, 如果有一个不合法, 那么整个表达式就为 false //在整个表达式全部合法的前提下, 才有意义谈|| && 才进入求值判断阶段 // || 表示有一个参数符合就返回true,&& 表示所有参数符合才返回true template <typename... Ts> concept Foo1 = (is_integral_v<typename Ts::type>||...);
如果你仔细看看这个表达式结构…oh my gosh! 这可不就是个折叠表达式么? C++ Template >> [ 5 ].模板的可变参数->Variadic Expressions
非可变模板参数如何形成原子约束?
template <typename T> concept Foo = bool(is_integral_v<typename T::type>||sizeof(T)>1);//原子约束
-
中间层
上面有关可变模板参数的 concept 想要表达 “至少一个模板参数存在类型成员 type 且类型成员为整型” 怎么办? 很简单, 创建一个约束中间层就行了
concept 的返回值总可能合法template <typename T>//中间层 concept Midal = is_integral_v<typename T::type>; template <typename... Ts> concept Foo1 = (Midal<Ts> ||...);
-
negation (逻辑否定)
template<typename T>concept C1 = is_integral_v<typename T::type>; template<typename T>concept C2 =! is_integral_v<typename T::type>; //T存在type且为整数类型时 C3 为 false, 反之只要C1中有一条不满足则C1为false, C3这时就为true template<typename T>concept C3 =! C1<T>;struct Foo{using type = float ;}; //static_assert(C1<int>);//C1 表达: 要求T存在关联类型type,且 关联类型为整数类型 //static_assert(C1<Foo>); static_assert(C2<Foo>);//C2 表达: 要求T存在关联类型type,且 关联类型不为整数类型 static_assert(C3<Foo>);//C3 表达: 要求T不存在关联类型type,或 关联类型不为整数类型 static_assert(C3<int>);//其实是把C1当作一个整体来看,只看结果
注意两者语义的细微差别
requires 表达式
模板元编程
constexpr 元编程
Ranges 标准库
协程
软件设计六大原则 SOLID
- Single responsibility principle
- Open Closed Principle
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle
- Dependence Inversion Principle
- Law of Demeter
- https://baike.baidu.com/starmap/view?nodeId=294b5d3c8cc7452ad5c9cdba&lemmaTitle=开闭原则&lemmaId=2828775&starMapFrom=lemma_starMap&fromModule=lemma_starMap
To be continue…
https://netcan.github.io/