文章目录
- 前言
- 前奏
- lambda
- 浅谈std::ref的实现
- 浅谈is_same
- 浅谈std::function的实现
- std::visit 与 std::variant 与运行时多态
- SFINAE
- 类型内省
- 标签分发 (tag dispatching)
- 软件设计六大原则 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.5664 std::cout << "shape area: " << shape->getArea() << " perimeter: " << shape->getPerimeter() << std::endl; shape = std::make_unique<Rectangle>(2, 3); // shape area: 6 perimeter: 10 std::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.5664 std::cout << "shape area: " << getArea(shape) << " perimeter: " << getPerimeter(shape) << std::endl; shape = Rectangle{2, 3}; // shape area: 6 perimeter: 10 std::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
类型内省
- 检查对象的类型或属性的一种能力
- 在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> {}); }
软件设计六大原则 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/