尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++模板编程:从函数模板到泛型工厂的完整指南

C++模板编程:从函数模板到泛型工厂的完整指南 1. 从“重复造轮子”到“一劳永逸”为什么我们需要C模板如果你写过一段时间的C尤其是在处理数据结构或者算法时大概率会经历过这种场景你需要一个函数来比较两个整数的大小于是你写了个int max(int a, int b)。过两天项目里又需要比较两个浮点数你复制粘贴了上面的代码把参数类型改成了double。紧接着字符串比较、自定义的Student对象按分数比较的需求接踵而至……很快你的代码库里就堆满了功能几乎一模一样、只是参数类型不同的函数。这不仅让代码变得臃肿更可怕的是当你发现max函数的边界条件处理有bug时你需要把所有重载版本都修改一遍维护成本直线上升。C模板Template就是为了解决这类“类型无关的通用代码”问题而生的。它本质上不是一段具体的代码而是一个“代码生成器”的蓝图。编译器会根据你使用模板时提供的具体类型现场为你“实例化”出一份针对该类型的、完全特化的代码。这就像你有一个做饼干的模具模板你可以用面粉int、巧克力粉double或者抹茶粉std::string作为原料模具本身不变但能生产出不同口味的饼干具体函数或类。很多人初学模板时觉得它语法古怪、难以调试是“高级特性”而敬而远之。但在我看来模板是现代C的基石是区分“C with Classes”程序员和真正C程序员的一道分水岭。它带来的不仅是代码复用更是一种强大的抽象能力和编译期计算的可能性。从简单的容器如std::vectorT到复杂的算法如std::sort再到元编程和概念C20 Concepts模板无处不在。掌握它你才能写出既高效又优雅的C代码。2. 函数模板你的第一个通用工具让我们从最直观的函数模板开始。它的目标很简单写一个函数声明让它的参数类型或返回类型可以是“任意”的。2.1 基础语法与实例化过程一个经典的函数模板例子就是上面提到的max// 声明一个函数模板 template typename T // 模板参数列表T是一个类型参数 T myMax(T a, T b) { // 函数声明和定义使用类型参数T return (a b) ? a : b; }这短短几行代码里蕴含了几个关键点template typename T 这是模板引入的关键字。typename也可以用class替代在这里两者等价它告诉编译器后面跟着的T是一个“占位符类型”也叫模板类型参数。T myMax(T a, T b) 函数签名。这里所有的T都指向同一个类型。这意味着调用myMax(1, 2)时T被推导为int调用myMax(3.14, 2.71)时T被推导为double。实例化Instantiation 这是模板的核心魔法。当你写下myMax(1, 2)时编译器并不会去调用一个叫myMax的泛型函数。相反它根据推导出的类型int以模板为蓝图在编译期生成一个实实在在的函数int myMax(int a, int b) { return (a b) ? a : b; }。这个过程对程序员是透明的但理解它对于调试模板错误至关重要。注意 模板的编译是“两阶段”的。第一阶段检查模板本身的语法如template关键字是否正确第二阶段在实例化时检查模板代码对于具体类型是否有效例如类型T是否支持操作。很多令人困惑的编译错误都发生在第二阶段。2.2 类型推导的规则与陷阱编译器是如何推导出T是什么类型的呢规则其实很直观但有几个陷阱需要留心。规则1根据函数实参推导这是最常见的情况。对于myMax(1, 2)两个实参都是int所以T被推导为int。陷阱1类型必须一致int a 5; double b 3.14; auto result myMax(a, b); // 编译错误这里第一个实参让T推导为int第二个让T推导为double编译器无法确定T到底是什么。解决方法有三种强制转换myMax(static_castdouble(a), b)显式指定模板参数myMaxdouble(a, b)。这是告诉编译器“别推导了就用double作为T来生成函数”。对于a会发生隐式类型转换。使用多个模板参数见下文。规则2处理引用和常量如果模板参数是引用类型推导规则会“忽略”引用但会保留const等限定符。templatetypename T void f(T param) {} int x 10; const int cx x; const int rx x; f(x); // T 推导为 int f(cx); // T 推导为 const int f(rx); // T 推导为 const int 引用被忽略2.3 多参数与默认模板参数一个模板可以有多个参数这大大增强了灵活性。// 两个类型参数可以不同 template typename T1, typename T2 auto smartMax(T1 a, T2 b) - decltype(a b ? a : b) { return (a b) ? a : b; } // 使用C14的自动返回类型更简洁 template typename T1, typename T2 auto smarterMax(T1 a, T2 b) { return (a b) ? a : b; } auto val smarterMax(5, 3.14); // 没问题T1int, T2double这里我们使用了auto作为返回类型让编译器根据return语句自行推导。这解决了混合类型比较的问题返回类型将是两个参数中“更宽”的那个类型例如int和double运算结果是double。和函数参数一样模板参数也可以有默认值。template typename T int, typename Container std::vectorT class MyArray { /*...*/ }; MyArray arr1; // 使用默认的 int 和 std::vectorint MyArraydouble arr2; // Tdouble, Containerstd::vectordouble这在设计通用库时非常有用可以为用户提供合理的默认行为。3. 类模板构建通用容器的骨架如果说函数模板让算法变得通用那么类模板就让数据结构变得通用。std::vectorstd::liststd::map这些都是类模板的经典代表。3.1 类模板的定义与成员函数实现我们来实现一个简化版的std::pair 它能够持有两个任意类型的值。// pair.h template typename T1, typename T2 // 类模板参数 class MyPair { public: // 构造函数使用成员初始化列表 MyPair(const T1 first, const T2 second) : first_(first), second_(second) {} // 获取第一个元素 T1 first() const { return first_; } // 获取第二个元素 T2 second() const { return second_; } // 一个简单的成员函数模板示例交换两个pair的内容 template typename U1, typename U2 void swap(MyPairU1, U2 other) { std::swap(first_, other.first_); std::swap(second_, other.second_); } private: T1 first_; T2 second_; };类模板的成员函数如果在类体内定义如上例的first()和second()它们会隐式地成为函数模板。如果在类体外定义语法会稍微复杂一些。3.2 在类外定义成员函数为了更好的代码组织我们常常将成员函数的定义放在类的外部比如在.cpp或.ipp文件中。这时每一个成员函数都需要被声明为模板。// pair.h template typename T1, typename T2 class MyPair { public: MyPair(const T1 first, const T2 second); T1 first() const; T2 second() const; void print() const; // 声明一个print函数 private: T1 first_; T2 second_; }; // 包含成员函数定义的实现文件通常命名为 pair.ipp 或直接写在 .h 末尾 #include “pair.ipp” // pair.ipp // 构造函数在类外的定义 template typename T1, typename T2 MyPairT1, T2::MyPair(const T1 first, const T2 second) : first_(first), second_(second) {} // first() 函数在类外的定义 template typename T1, typename T2 T1 MyPairT1, T2::first() const { return first_; } // print() 函数在类外的定义 template typename T1, typename T2 void MyPairT1, T2::print() const { std::cout “(” first_ “, ” second_ “)” std::endl; }关键语法MyPairT1, T2::这个作用域指明我们正在定义的是MyPairT1, T2这个特定实例化类的成员函数。每个成员函数定义本身也是一个模板因此需要以template typename T1, typename T2开头。实操心得 模板代码几乎总是必须放在头文件.h或.hpp中或者被头文件包含如.ipp文件。因为模板是编译期生成的编译器在编译使用模板的源文件如main.cpp时必须能看到模板的完整定义才能进行实例化。如果像普通函数一样把定义放在.cpp文件里并单独编译链接时会找不到实例化后的函数实体导致“未定义的引用”错误。这是模板初学者最常踩的坑之一。3.3 静态成员与友元类模板也可以有静态成员但需要特别注意每个不同的模板实例化都拥有自己独立的静态成员副本。template typename T class MyClass { public: static int count; // 静态成员声明 MyClass() { count; } }; // 静态成员的定义。注意它本身也是模板 template typename T int MyClassT::count 0; // 使用 MyClassint a, b; // MyClassint::count 现在是 2 MyClassdouble c; // MyClassdouble::count 现在是 1 (独立于上面的count)友元声明在模板类中会更复杂一些。如果你想让一个特定的类或函数成为所有MyPair实例的友元可以这样写template typename T1, typename T2 class MyPair { // 声明一个普通函数printPair是所有MyPair实例的友元 friend void printPair(const MyPairT1, T2 p); private: T1 first_; T2 second_; }; // 注意printPair函数需要针对每一对(T1, T2)类型进行定义。更复杂的情况是声明一个函数模板为友元语法会变得棘手通常需要前向声明。在实际开发中除非有强烈需求否则应谨慎使用模板类的友元以保持接口的清晰。4. 非类型模板参数与模板特化模板参数不仅仅是类型typename T还可以是整型、枚举、指针或引用等“非类型”参数。4.1 非类型模板参数的应用一个经典的例子是固定大小的数组类类似于std::array。template typename T, std::size_t N // N 是一个非类型模板参数必须是编译期常量 class FixedArray { public: T operator[](std::size_t index) { if (index N) throw std::out_of_range(“Index out of range”); return data_[index]; } const T operator[](std::size_t index) const { /*...*/ } std::size_t size() const { return N; } // 大小在编译期就确定了 private: T data_[N]; // 栈上分配的固定大小数组 }; // 使用 FixedArrayint, 10 arr; // 一个包含10个int的数组 FixedArraydouble, 100 bigArr; // 一个包含100个double的数组 // FixedArrayint, n dynArr; // 错误n必须是编译期常量非类型模板参数的限制 它的值必须在编译期就知道。这带来了极高的性能优势编译器可以进行边界检查优化、循环展开等但也失去了运行时的灵活性。4.2 全特化为特定类型定制行为有时候泛化的模板逻辑对某些特定类型并不合适。例如我们有一个比较是否相等的泛型函数template typename T bool isEqual(T a, T b) { return a b; }对于浮点数float或double直接使用比较由于精度问题可能是不安全的。这时我们可以为double类型提供一个**特化Specialization**版本。// 通用的主模板 template typename T bool isEqual(T a, T b) { std::cout “Using generic comparison.” std::endl; return a b; } // 为 double 类型提供的全特化版本 template // 注意这里模板参数列表为空 bool isEqualdouble(double a, double b) { std::cout “Using specialized double comparison.” std::endl; const double epsilon 1e-9; return std::fabs(a - b) epsilon; } // 使用 isEqual(1, 2); // 调用通用版本Tint isEqual(3.14, 3.14); // 调用 double 特化版本 isEqual(“hello”, “world”); // 调用通用版本Tconst char*全特化相当于为模板参数列表提供了一个“完全具体”的版本它不再是一个模板而是一个普通的函数/类。编译器在匹配时会优先选择最特化的版本。4.3 偏特化针对类型模版的局部定制偏特化Partial Specialization允许我们为模板参数的一部分进行特化它只适用于类模板不适用于函数模板函数模板可以通过重载实现类似效果。假设我们有一个通用的“指针包装器”类模板template typename T class PtrWrapper { public: void process() { std::cout “Processing value: ” *ptr_ std::endl; } private: T* ptr_; };对于T*这种本身就是指针的类型比如int**上面的逻辑可能不合适。我们可以为所有指针类型提供一个偏特化版本// 主模板 template typename T class PtrWrapper { /*...*/ }; // 偏特化当T是一个指针类型U*时使用这个版本 template typename U class PtrWrapperU* { // 注意语法模板参数是U但特化的是 PtrWrapperU* public: void process() { std::cout “Processing pointer to pointer.” std::endl; } private: U** ptr_; // 这里变成了指向指针的指针 };当使用PtrWrapperint时匹配主模板。当使用PtrWrapperint*时匹配偏特化版本U被推导为int。偏特化极大地增强了模板的灵活性和表现力是设计通用库如STL的重要工具。5. 可变参数模板处理任意数量的参数C11引入的可变参数模板Variadic Templates是模板元编程的一座里程碑。它允许模板接受任意数量、任意类型的参数包为编写像std::tuplestd::function 以及完美转发等高级功能奠定了基础。5.1 语法基础与参数包展开可变参数模板使用省略号...来表示一个“模板参数包”。// Args 是一个模板参数包可以包含0个或多个类型 template typename... Args void print(Args... args) { // args 是一个函数参数包 // 无法直接操作参数包必须通过展开来使用 }单独的参数包没有用处我们需要“展开”它。最常见的方式是使用递归。// 递归的基准情形0个参数 void print() { std::cout std::endl; } // 可变参数模板版本 template typename T, typename... Args // 分离第一个参数和剩下的包 void print(T first, Args... rest) { std::cout first “ ”; // 处理第一个参数 print(rest...); // 递归调用展开剩下的参数包 } // 使用 print(1, 3.14, “hello”); // 输出1 3.14 hello编译器会实例化出printint, double, const char* 然后递归实例化printdouble, const char* 最后调用无参数的print()结束递归。5.2 折叠表达式更简洁的展开方式C17引入了折叠表达式Fold Expression让一些常见的参数包操作变得异常简洁无需递归。// 使用折叠表达式计算所有参数的和要求所有参数类型支持操作 template typename... Args auto sum(Args... args) { return (args ...); // 一元右折叠(arg1 (arg2 (arg3 ...))) // 也可以写成左折叠(... args) 对应 (((arg1 arg2) arg3) ...) } // 使用折叠表达式调用同一个函数 template typename... Args void callOnEach(Args... args) { (process(args), ...); // 逗号运算符折叠依次对每个参数调用process函数 }折叠表达式极大地简化了可变参数模板的代码可读性也更强。5.3 实战实现一个简易的Tuplestd::tuple是可变参数模板的经典应用。我们来窥探一下它的简化实现原理。// 空Tuple作为递归基 template typename... Types class MyTuple; // 特化版本至少有一个元素的情况 template typename Head, typename... Tail class MyTupleHead, Tail... : private MyTupleTail... { // 递归继承 public: MyTuple(const Head head, const Tail... tail) : MyTupleTail...(tail...), head_(head) {} // 获取第一个元素需要类型转换 Head getHead() { return head_; } const Head getHead() const { return head_; } // 获取剩余部分基类就是剩下的Tuple MyTupleTail... getTail() { return *this; } const MyTupleTail... getTail() const { return *this; } private: Head head_; }; // 使用 MyTupleint, double, std::string t(1, 2.0, “three”); auto i t.getHead(); // int 1 auto d t.getTail().getHead(); // double 2.0 auto s t.getTail().getTail().getHead(); // string “three”这个实现利用了“递归继承”和“偏特化”。MyTupleint, double, string继承自MyTupledouble, string 后者又继承自MyTuplestring 最终继承自空的MyTuple。每个派生类存储自己的“头”元素并通过基类访问剩下的元素。虽然这个实现非常简陋没有std::getN这样的编译期索引但它清晰地展示了可变参数模板如何用于构建复杂的数据结构。6. 模板元编程基础与SFINAE模板不仅仅能生成代码还能在编译期执行计算和做出决策这就是模板元编程Template Metaprogramming TMP。它利用模板实例化机制将运行时的计算转移到编译期。6.1 编译期计算示例阶乘最著名的例子是编译期计算阶乘。// 主模板通用情况声明但不定义对于非正整数N这个模板不会被匹配 template unsigned int N struct Factorial { static const unsigned long long value N * FactorialN - 1::value; }; // 全特化递归的基准情形 template struct Factorial0 { static const unsigned long long value 1; }; // 使用 int main() { // 计算发生在编译期运行时只是一个常量 constexpr auto fact5 Factorial5::value; // 120 constexpr auto fact10 Factorial10::value; // 3628800 // Factorial-1::value; // 编译错误没有匹配的模板 }Factorial5::value在编译期就会被计算为5 * 4 * 3 * 2 * 1 120。这展示了模板如何通过递归实例化来进行计算。现代C中constexpr函数在很多场景下可以更直观地替代这类TMP但理解其原理至关重要。6.2 SFINAE替换失败并非错误SFINAESubstitution Failure Is Not An Error是模板重载决议的核心规则。它的意思是在模板参数推导和重载匹配过程中如果某个模板的实例化会导致编译错误比如无效的表达式或类型编译器不会把它当作一个错误而终止而是简单地把它从候选集中剔除继续尝试其他可行的重载。这听起来很拗口但它是实现“根据类型是否有某个属性来选择不同实现”的关键。在C11之前SFINAE通常借助sizeof、decltype和返回类型来实现代码非常晦涩。// 一个经典的SFINAE例子检测类型T是否有名为 size 的成员函数 template typename T class has_size_method { private: // 两个返回类型不同的辅助函数 template typename U static auto test(int) - decltype(std::declvalU().size(), std::true_type{}); template typename U static std::false_type test(...); public: static constexpr bool value decltype(testT(0))::value; }; // 使用 std::cout has_size_methodstd::vectorint::value; // 输出 1 (true) std::cout has_size_methodint::value; // 输出 0 (false)这段代码利用了重载优先级int参数匹配优先级高于...可变参数和decltype在无效表达式时会导致推导失败SFINAE的特性。如果T有.size()成员第一个test函数有效匹配它并返回std::true_type否则匹配第二个返回std::false_type的函数。6.3 使用std::enable_if进行条件编译std::enable_if是SFINAE思想的一个标准化、更易用的工具。它通常用于函数模板的返回类型或额外的模板参数上来有条件地启用或禁用某个模板。// 一个函数只对可以转换为字符串的类型有效 template typename T typename std::enable_ifstd::is_convertibleT, std::string::value, void::type printIfConvertible(const T val) { std::cout std::string(val) std::endl; } // 另一个版本针对其他类型比如整数做不同处理 template typename T typename std::enable_ifstd::is_integralT::value, void::type printIfConvertible(const T val) { std::cout “Integral: ” val std::endl; } // 使用 printIfConvertible(“Hello”); // 调用第一个版本 printIfConvertible(42); // 调用第二个版本 // printIfConvertible(std::vectorint{}); // 编译错误没有匹配的模板std::enable_ifCondition, Type在Condition为true时其::type成员就是Type否则它没有::type成员导致替换失败SFINAE该函数模板被从候选集中移除。虽然SFINAE和enable_if功能强大但它们也让代码变得复杂难懂。C20引入的概念Concepts正是为了从根本上解决这个问题它允许我们直接用清晰、可读的约束来表达对模板参数的要求是未来编写模板代码的首选方式。7. 现代C中的模板从auto到概念现代CC11/14/17/20为模板编程带来了诸多福音极大地简化了代码并提升了安全性。7.1auto与decltype让类型推导无处不在auto关键字在C11中被赋予了新的生命用于自动推导变量类型。在模板编程中它经常与decltype配合使用。template typename Container auto getFirstElement(const Container c) - decltype(c.front()) { // 返回类型后置使用decltype根据表达式c.front()推导返回类型 if (!c.empty()) return c.front(); throw std::out_of_range(“Container is empty”); }C14允许auto作为函数返回类型编译器会自动推导。template typename T, typename U auto add(T a, U b) { // 返回类型自动推导为 decltype(a b) return a b; }decltype(auto)则用于完美转发返回类型保留值类别左值/右值和引用属性。template typename F, typename... Args decltype(auto) callAndReturn(F f, Args... args) { return std::forwardF(f)(std::forwardArgs(args)...); }7.2 别名模板与变量模板using关键字可以创建类型别名结合模板就是别名模板它比传统的typedef更清晰易读尤其是在涉及模板的时候。template typename T using Vec std::vectorT; // Vecint 等价于 std::vectorint template typename Key, typename Value using Map std::mapKey, Value, std::lessKey, MyAllocatorstd::pairconst Key, Value; // 简化复杂类型C14引入了变量模板允许我们定义模板化的常量。template typename T constexpr T pi T(3.1415926535897932385L); float area pifloat * r * r; double circumference 2 * pidouble * r;7.3 C20 概念模板约束的革命概念Concepts是C20最令人期待的特性之一它允许我们为模板参数指定必须满足的约束条件将编译错误从模板实例化的深处提前到接口声明处并让错误信息变得清晰可读。// 定义一个概念要求类型T有 size() 方法且返回值为 size_t template typename T concept HasSize requires(T t) { { t.size() } - std::convertible_tostd::size_t; }; // 使用概念约束模板 template HasSize Container void printSize(const Container c) { std::cout “Size: ” c.size() std::endl; } // 或者更简洁的写法C20 void printSize(const HasSize auto c) { std::cout “Size: ” c.size() std::endl; } // 使用 std::vectorint v{1,2,3}; printSize(v); // 正确 // printSize(42); // 编译错误清晰提示int 不满足 HasSize 约束概念彻底改变了模板编程的体验。它取代了复杂的SFINAE技巧让意图更明确代码更健壮错误信息也从几十页的“天书”变成了直白的“int没有size()成员”。虽然目前编译器支持仍在完善中但它无疑是C模板未来的方向。8. 模板实战设计一个简单的泛型工厂模式最后让我们综合运用所学设计一个简单的泛型对象工厂。这个工厂可以根据一个字符串键如类名和一系列构造参数创建出对应的对象。这在插件系统、反序列化等场景中非常有用。8.1 工厂的核心注册表工厂的核心是一个映射将字符串标识符关联到一个“创建函数”上。由于要创建不同类型的对象我们需要一个能返回任意类型的函数这自然想到了std::function和模板。#include unordered_map #include string #include functional #include memory #include iostream class GenericFactory { public: // 创建函数的类型接受任意参数包返回一个unique_ptr指向基类Base template typename Base, typename... Args using CreatorFunc std::functionstd::unique_ptrBase(Args...); // 注册创建函数 template typename Base, typename Derived, typename... Args static bool registerCreator(const std::string key) { // 检查键是否已存在 auto map getMapBase, Args...(); if (map.find(key) ! map.end()) { std::cerr “Key \”” key “\” already registered.” std::endl; return false; } // 注册一个lambda它使用Derived的构造函数 map[key] [](Args... args) - std::unique_ptrBase { return std::make_uniqueDerived(std::forwardArgs(args)...); }; std::cout “Registered \”” key “\” for type ” typeid(Base).name() std::endl; return true; } // 创建对象 template typename Base, typename... Args static std::unique_ptrBase create(const std::string key, Args... args) { auto map getMapBase, Args...(); auto it map.find(key); if (it map.end()) { std::cerr “Key \”” key “\” not found.” std::endl; return nullptr; } // 调用注册的创建函数 return it-second(std::forwardArgs(args)...); } private: // 获取或创建特定Base和Args组合的注册表映射 template typename Base, typename... Args static auto getMap() { // 使用一个以类型本身作为键的静态map。每个不同的(Base, Args...)组合都有独立的map。 static std::unordered_mapstd::string, CreatorFuncBase, Args... instance; return instance; } };这个实现的关键点在于getMap这个静态函数模板。它为每一组(Base, Args...)类型组合维护了一个独立的静态注册表。这确保了为BaseA注册的创建函数不会和BaseB的混淆。8.2 使用示例与类型擦除的挑战假设我们有一个动物基类和几个派生类。class Animal { public: virtual ~Animal() default; virtual void speak() const 0; }; class Dog : public Animal { public: explicit Dog(const std::string name) : name_(name) {} void speak() const override { std::cout name_ “ says: Woof!” std::endl; } private: std::string name_; }; class Cat : public Animal { public: explicit Cat(int lives) : lives_(lives) {} void speak() const override { std::cout “Cat with ” lives_ “ lives says: Meow!” std::endl; } private: int lives_; }; // 注册 bool reg1 GenericFactory::registerCreatorAnimal, Dog, const std::string(“dog”); bool reg2 GenericFactory::registerCreatorAnimal, Cat, int(“cat”); // 使用 auto myDog GenericFactory::createAnimal(“dog”, “Buddy”); if (myDog) myDog-speak(); // 输出Buddy says: Woof! auto myCat GenericFactory::createAnimal(“cat”, 9); if (myCat) myCat-speak(); // 输出Cat with 9 lives says: Meow! auto unknown GenericFactory::createAnimal(“bird”, 1); // 输出错误Key “bird” not found.这个工厂是类型安全的。尝试用错误的参数调用create例如createAnimal(“dog”, 123)会导致编译错误因为int无法转换为const std::string 从而无法匹配到注册时指定的CreatorFuncAnimal, const std::string类型的映射。8.3 进阶思考与局限性这个简单工厂展示了模板在构建灵活、类型安全系统方面的强大能力。但它也有局限性注册必须在运行时之前完成 我们的注册发生在静态初始化阶段通过静态变量reg1reg2。更动态的注册需要额外的机制。构造参数必须精确匹配create的参数类型必须与注册时registerCreator指定的Args...完全匹配。这限制了灵活性比如无法处理默认参数。单例模式 工厂本身是静态的全局唯一。在某些需要多个工厂实例的场景下需要调整。尽管如此这个模式的核心思想——将“字符串键”映射到“泛型创建函数”——是许多大型框架和库的基础。通过模板我们实现了高度的复用性和类型安全避免了使用裸指针和void*带来的风险。理解并能够设计这样的模式标志着你对C模板的理解已经达到了一个相当实用的深度。
返回列表