diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs index b0a1483..61f01f5 100644 --- a/.vitepress/config.mjs +++ b/.vitepress/config.mjs @@ -4,6 +4,7 @@ import footnote_plugin from 'markdown-it-footnote'; const tutorial_path = '/md/第一部分-基础知识/'; const tutorial2_path = '/md/第二部分-造轮子/'; +const tutorial3_path = '/md/扩展知识/'; const homework_path = '/homework/08折叠表达式作业/'; // https://vitepress.dev/reference/site-config @@ -41,6 +42,13 @@ export default defineConfig({ { text: 'ooolize', link: homework_path + 'ooolize' }, ] }, + { + text: '扩展知识', + collapsed: true, + items: [ + {text: `CRTP的原理与使用`, link: tutorial3_path + `CRTP的原理与使用` }, + ] + }, { text: '造轮子', collapsed: true, diff --git "a/md/\346\211\251\345\261\225\347\237\245\350\257\206/CRTP\347\232\204\345\216\237\347\220\206\344\270\216\344\275\277\347\224\250.md" "b/md/\346\211\251\345\261\225\347\237\245\350\257\206/CRTP\347\232\204\345\216\237\347\220\206\344\270\216\344\275\277\347\224\250.md" new file mode 100644 index 0000000..d75cf7e --- /dev/null +++ "b/md/\346\211\251\345\261\225\347\237\245\350\257\206/CRTP\347\232\204\345\216\237\347\220\206\344\270\216\344\275\277\347\224\250.md" @@ -0,0 +1,128 @@ +# CRTP 奇特重现模板模式 + +它的范式基本上都是:父类是模板,再定义一个类类型继承它。因为类模板不是类,只有实例化后的类模板才是实际的类类型,所以我们需要实例化它,**显式指明模板类型参数,而这个类型参数就是我们定义的类型,也就是子类了**。 + +```cpp +template +class Base {}; + +class X : public Base {}; +``` + +这种范式是完全合法合理的,并无问题,首先不要对这种东西感到害怕或觉得非常神奇,也只不过是基本的语法规则所允许的。即使不使用 `CRTP` ,这些写法也是完全合理的,并无问题。 + +--- + +CRTP 可用于在父类暴露接口,而子类实现该接口,以此实现“***编译期多态***”,或称“***静态多态***”。 示例如下: + +```cpp +template +class Base { +public: + // 公开的接口函数 供外部调用 + void addWater(){ + // 调用子类的实现函数。要求子类必须实现名为 impl() 的函数 + // 这个函数会被调用来执行具体的操作 + static_cast(this)->impl(); + } +}; + +class X : public Base { +public: + // 子类实现了父类接口 + void impl() const{ + std::cout<< "X 设备加了 50 毫升水\n"; + } +}; +``` + +使用方式也很简单,我们直接创建子类对象,调用 `addWater` 函数即可: + +```cpp +X x; +x.addWater(); +``` + +> [运行](https://godbolt.org/z/o373avza5)测试。 + +那么好,问题来了,**为什么呢?** `static_cast(this)` 是做了什么,它为什么可以这样? + +- 很显然 `static_cast(this)` 是进行了一个类型转换,将 `this` 指针(也就是**父类的指针**),转换为通过模板参数传递的类型,也就是**子类的指针**。 +- **这个转换是安全合法的**。因为 this 指针实际上指向一个 X 类型的对象,X 类型对象继承了 `Base` 的部分,X 对象也就包含了 `Base` 的部分,所以这个转换在编译期是有效的,并且是合法的。 +- 当你调用 `x.addWater()` 时,实际上是 X 对象调用了父类 `Base` 的成员函数。这个成员函数内部使用 `static_cast(this)`,将 this 从 `Base*` 转换为 `X*`,然后调用 X 中的 impl() 函数。这种转换是合法且安全的,且 X 确实实现了 impl() 函数。 + +当然了,我们给出的示例是十分简单的,不过大多的使用的确也就是如此了,我们可以再优化一点,比如不让子类的接口暴露出来: + +```cpp +template +class Base { +public: + void addWater(){ + static_cast(this)->impl(); + } +}; + +class X : public Base { + // 设置友元,让父类得以访问 + friend Base; + // 私有接口,禁止外部访问 + void impl() const{ + std::cout<< "X 设备加了 50 毫升水\n"; + } +}; +``` + +## C++23 的改动-显式对象形参 + +C++23 引入了**显式对象形参**,让我们的 `CRTP` 的形式也出现了变化: + +> [显式对象形参](https://zh.cppreference.com/w/cpp/language/member_functions#.E6.98.BE.E5.BC.8F.E5.AF.B9.E8.B1.A1.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0),顾名思义,就是将 C++23 之前,隐式的,由编译器自动将 `this` 指针传递给成员函数使用的,改成**允许用户显式写明**了,也就是: +> +> ```cpp +> struct X{ +> void f(this const X& self){} +> }; +> ``` +> +> 它也支持模板(可以直接 `auto` 而无需再 `template`),也支持各种修饰,如:`this X self`、`this X& self`、`this const X& self`、`this X&& self`、`this auto&& self`、`const auto& self` ... 等等。 + +```cpp +struct Base { void name(this auto&& self) { self.impl(); } }; +struct D1 : Base { void impl() { std::puts("D1::impl()"); } }; +struct D2 : Base { void impl() { std::puts("D2::impl()"); } }; +``` + +不再需要使用 `static_cast` 进行转换,直接调用即可。且如你所见,我们的显式对象形参也可以写成模板的形式:`this auto&& self`。 + +使用上也与之前并无区别,创建子类对象,调用接口即可。 + +```cpp +D1 d; +d.name(); +D2 d2; +d2.name(); +``` + +`d.name` 也就是把 `d` 传入给父类模板成员函数 `name`,`auto&&` 被推导为 `D1&`,顾名思义”***显式***“对象形参,非常的简单直观。 + +> [运行](https://godbolt.org/z/WW59PqEd3)测试。 + +## CRTP 的好处 + +上一节我们详细的介绍和解释了 CRTP 的编写范式和原理。现在我们来稍微介绍一下 CRTP 的众多好处。 + +1. **静态多态** + + CRTP 实现静态多态,无需使用虚函数,静态绑定,无运行时开销。 +2. **类型安全** + + CRTP 提供了类型安全的多态性。通过模板参数传递具体的子类类型,编译器能够确保类型匹配,避免了传统向下转换可能引发的类型错误。 +3. **灵活的接口设计** + + CRTP 允许父类定义公共接口,并要求子类实现具体的操作。这使得基类能够提供通用的接口,而具体的实现细节留给派生类。其实也就是说多态了。 + +## 总结 + +事实上笔者所见过的 `CRTP` 的用法也还不止如此,还有许多更加复杂,有趣的做法,不过就不想再探讨了,以上的内容已然足够。其它的做法也无非是基于以上了。 + +各位可以尝试尽可能的将使用虚函数的代码改成 `CRTP` ,这其实在大多数时候并不构成难度,绝大多数的多态类型都能被很轻松的改成 `CRTP` 的形式。 diff --git "a/md/\347\254\254\344\270\200\351\203\250\345\210\206-\345\237\272\347\241\200\347\237\245\350\257\206/10\344\272\206\350\247\243\344\270\216\345\210\251\347\224\250SFINAE.md" "b/md/\347\254\254\344\270\200\351\203\250\345\210\206-\345\237\272\347\241\200\347\237\245\350\257\206/10\344\272\206\350\247\243\344\270\216\345\210\251\347\224\250SFINAE.md" index e0a3ef7..ba6c4df 100644 --- "a/md/\347\254\254\344\270\200\351\203\250\345\210\206-\345\237\272\347\241\200\347\237\245\350\257\206/10\344\272\206\350\247\243\344\270\216\345\210\251\347\224\250SFINAE.md" +++ "b/md/\347\254\254\344\270\200\351\203\250\345\210\206-\345\237\272\347\241\200\347\237\245\350\257\206/10\344\272\206\350\247\243\344\270\216\345\210\251\347\224\250SFINAE.md" @@ -165,14 +165,41 @@ using enable_if_t = typename enable_if::type; // void 是默认模 --- -再谈,std::enable_if 的默认模板实参是 **`void`**,如果我们不在乎 std::enable_if 得到的类型,就让它默认就行,比如我们的示例 `f` 根本不在乎第二个模板形参 `SFINAE` 是啥类型。 +再谈,`std::enable_if` 的默认模板实参是 **`void`**,如果我们不在乎 `std::enable_if` 得到的类型,就让它默认就行,比如我们先前的示例 `f` 根本不在乎第二个模板形参 `SFINAE` 是啥类型。 + +此外,`std::enable_if` 还有一种常见的用法,即利用其第二个模板参数而不声明额外的模板类型参数。例如: + +```cpp +template,int> =0> +void f(T){} +``` + +它的作用和之前的写法是一样的,但这个写法的原理是什么呢?我们可以逐步解析: + +```cpp +std::enable_if_t,int> =0 +``` + +这里的 `=0` 实际上是对前面 `enable_if_t` 表达式的默认实参,它起到的是无名默认实参的作用。也就是说,如果 `std::is_same_v` 为 true,那么 `std::enable_if_t` 变为: + +```cpp +using enable_if_t = typename enable_if::type; +``` + +`true` 会选择 enable_if 的偏特化,从而**有 `type` 别名**,它的类型就是是我们传入的第二个参数 `int`。因此,**`std::enable_if_t` 实际上就是 int**。 + +当然,如果 `std::is_same_v` 为 `false`,则 `std::enable_if_t` 会导致**代换失败**。 +不过因为“代换失败不是错误”,所以只是不选择函数模板 `f`,而不会导致编译错误。(当然了,如果没有一个符合条件的重载,那还是会报编译错误的:“*未找到匹配的重载函数*”)。 + +--- ```cpp template array(Type, Args...) -> array && ...), Type>, sizeof...(Args) + 1>; ``` -以上示例,是显式指明了 std::enable_if 的第二个模板实参,为 `Type`。 +以上示例,是显式指明了 `std::enable_if` 的第二个模板实参,为 `Type`。 它是我们[类模板](02类模板.md)推导指引那一节的示例的**改进版本**,我们使用 std::enable_if_t 与 C++17 折叠表达式,为它增加了约束,这几乎和 [libstdc++](https://github.com/gcc-mirror/gcc/blob/7a01cc711f33530436712a5bfd18f8457a68ea1f/libstdc%2B%2B-v3/include/std/array#L292-L295) 中的代码一样。