C++ 基础

C++ 基础
泛化编程在 C 中泛型编程Generic Programming是一个非常核心且庞大的概念。它的本质是“编写独立于具体类型的代码”。泛型编程分为编译阶段的静态泛型与代码运行时的动态泛型静态泛型空间换时间编译器在编译阶段聪明地处理好泛型实例化工作。动态泛型时间换空间代码内置机关暗盒存放有跟踪tag和对象的辨别最总找到随身合适的版本。典型地C 继承多态里根据虚函数表这个机关暗盒背地里定位到合适的版本。除了你提到的函数模版之外C 还提供了非常丰富的工具来实现泛型编程。根据它们起作用的时机主要可以分为编译期泛型现代 C 的主力和运行期泛型。一、 编译期泛型静态多态这类实现和函数模版的原理一样都是在编译阶段由编译器生成具体代码运行效率极高零成本抽象。1. 类模版Class Template不仅函数可以泛型类也可以。这是 C 标准模板库STL的基石。容器如std::vector、std::list和工具类如std::unique_ptr都是通过类模版实现的。template typename T class Stack { private: std::vectorT elems; public: void push(T const elem) { elems.push_back(elem); } T pop() { /* ... */ } }; // 使用时实例化 Stackint intStack; // 编译期生成一个 int 版的栈 Stackstd::string strStack; // 编译期生成一个 string 版的栈2. 模版特化与偏特化Template Specialization有时候某种特定类型不能直接套用通用模版的逻辑例如char*字符串的比较不能直接用而要用strcmp。C 允许你为特定类型编写“特殊版本”。// 通用模版 template typename T bool isEqual(T a, T b) { return a b; } // 全特化专门针对 const char* 的版本 template bool isEqualconst char*(const char* a, const char* b) { return strcmp(a, b) 0; }3. 概念与约束Concepts / Constraints——C20 重磅特性在以前模版里的T可以是任何类型如果传入了不支持的类型编译器会报出长达几百行的极其难懂的错误信息。C20 引入了Concepts允许我们给泛型加上限制条件。#include concepts // 限制 T 必须是支持排序的类型 template typename T requires std::sortableT void sortContainer(T container) { // ... }4. 泛型 Lambda 表达式Generic Lambdas——C14Lambda 表达式匿名函数也可以是泛型的使用auto关键字作为参数类型编译器会自动把它转化为模版调用。auto print [](auto x) { std::cout x \n; }; print(10); // 隐式推导为 int print(hello); // 隐式推导为 string5. 变量模版Variable Templates——C14不仅函数和类连变量也可以是模版。常用于定义数学常数或类型属性。templatetypename T constexpr T pi T(3.1415926535897932385L); auto pi_f pifloat; // float 类型 auto pi_d pidouble; // double 类型二、 运行期泛型动态多态虽然 C 偏爱编译期泛型但有时我们也需要在程序运行的时候才能决定处理什么类型。1. 虚函数与继承面向对象的多态通过基类指针或引用指向派生类对象。虽然这属于面向对象OOP范畴但它解决的也是“用同一套代码处理不同类型对象”的问题。2.std::any和std::variant——C17 类型安全的类型擦除std::variant类型安全的联合体必须指定可能出现的类型范围。std::any可容纳任意类型类似于传统 C 语言中的void*但它是类型安全的知道自己当前存的是什么。#include variant #include any std::variantint, double, std::string v; v 10; // 现在存的是 int v hello; // 现在存的是 string std::any a 10; a std::string(world);总结C 泛型谱系图实现方式作用时期核心机制优缺点函数/类模版编译期代码生成实例化性能极致但可能导致二进制体积膨胀Concepts (C20)编译期模版接口约束提升模版代码的可读性和编译报错友好度虚函数/继承运行期虚函数表 (vtable)灵活性高但有运行期虚表查询开销std::variant/any运行期类型擦除/动态分配适合处理完全动态的数据类型在现代 CC11 及以后的开发中模版 Concepts的编译期泛型是绝对的主流。三、具体泛型编程实例3.1 函数模版1.C 模板函数的泛化完全是编译阶段发生的行为模版就是我们代码定义的蓝图函数模版的实现原理就是在编译阶段按最小化需求把这个蓝图实例化为具体的函数集这组函数集就是泛化参数的具体类型和修饰后的函数名字不同其他都一样。说大白话也就是自己设定好函数模版编译的时候编译器聪明地根据代码实际会调用到的类型逐个实例化这样就省去了自己硬编码。优点零成本抽象 - Zero-cost Abstraction运行速度极快因为在运行的时候根本没有所谓的“泛型”概念。print(10)在运行期就是直接调用print_int没有运行期的类型检查或装箱拆箱Box/Unbox开销性能和纯手写三个函数一模一样。缺点代码膨胀 - Code Bloat如果你用了一百种不同的类型去调用这个模版编译器就会生成一百份函数代码。这会导致最终编译出来的程序体积Binary Size变大。举例说明C 模版函数我们在泛化的时候 templatetypename T这些泛型是编译器阶段做的把能泛化的类型都实例化。到最后的代码就是很多个实例 int double string 假设这三种那么编译后产生的也是三种 函数代码存在于程序内。函数名修饰Name Mangling既然编译器生成了多个同名函数程序运行怎么区分编译器会根据实例化后的参数类型对函数名进行重命名。例如maxint可能会被转换成类似_Z3maxIiET_S0_S0_的内部符号确保每个实例都有唯一的地址。按需实例化Lazy Instantiation编译器非常吝啬。如果你写了一个模版但代码里从来没有调用过它那么最终的可执行文件中连一个字节的模版代码都不会存在。2. 模版的定义通常必须写在头文件.h里而不能像普通函数那样写在.cpp 文件里的原因普通函数编译时只需知道签名链接器Linker会在最后把调用处和实现处“组装”起来。模版函数编译器在编译调用它的.cpp文件时必须立刻看到模版的完整源码否则它无法根据类型如int在后台生成具体的函数代码。如果只看到声明编译器就会报错或者在链接时报错因为找不到具体实例化的代码。3.2 可泛型的 Lambda 表达式什么是 Lambda 表达式在 C 标准里Lambda 表达式的定义可以非常精确地概括为Lambda 表达式是一个纯右值C11 到 C17/ 纯右值或亡值C20 起的表达式它会在当前作用域原地构造出一个匿名的、独一无二的函数对象闭包对象。这个定义里藏着三个关键点它是表达式意味着它可以写在任何允许表达式出现的地方比如函数调用的实参里。它产生一个对象这个对象不是函数指针而是一个实打实的类实例函数对象。它是匿名的这个对象的类型是编译器自动生成的你在代码里写不出它的真实名字。C11 刚引入的 Lambda 表达式并不是什么颠覆底层运行机制的新技术而是编译器帮你做的一套自动化语法糖。它把原本需要手写的“类定义 实例化对象”的工作简化成了几行行内代码从而让 C 开发者终于能够优雅地编写高阶函数和事件回调。注意 Lambda 表达式本身不是泛型编程但是 Lambda 表达式在 C 14 可以被泛化编程作为操作对象。