十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++模板编程:从函数模板到类模板的泛型编程实战

C++模板编程:从函数模板到类模板的泛型编程实战 1. 项目概述从“函数模板”到“类模板”的思维跃迁在C的面向对象世界里我们刚熟悉了用类和对象来封装数据和操作构建出一个个清晰的实体模型。但很快你就会发现当需要处理不同类型数据却执行相同逻辑时比如写一个通用的“比较大小”或“数据交换”功能为每个类型int,double,string...都重写一遍代码不仅枯燥更违背了“代码复用”这一核心原则。这时模板Template就登场了它是C实现泛型编程的利器。简单说模板就是一份“代码蓝图”编译器能根据你使用的具体类型自动“印刷”出对应的、类型安全的代码版本。今天我们就聚焦在面向对象基础上的两大模板核心函数模板与类模板。这不仅是语法学习更是一种编程思维的升级——从处理具体类型到抽象出通用算法和数据结构。理解了它们你才能看懂STL标准模板库中vectorT,sort等强大工具背后的魔法并为后续学习更高级的模板特性如特化、可变参数模板打下坚实基础。无论你是正在刷题的学生还是希望写出更优雅、更健壮代码的开发者掌握模板都是通往C进阶的必经之路。2. 核心概念解析泛型编程的基石在深入代码之前我们必须厘清几个核心概念理解“为什么需要模板”比“怎么写模板”更重要。2.1 面向对象与泛型编程互补的哲学面向对象OOP通过封装、继承、多态主要解决的是数据抽象和行为多态的问题。它关注的是“是什么”以及对象之间的层次关系。例如我们定义一个Shape基类派生出Circle和Rectangle用虚函数draw()实现多态绘制。而泛型编程Generic Programming则关注于算法和数据结构的抽象它要解决的是“如何操作”的问题并且希望这种操作能独立于任何特定的数据类型。它的核心思想是将算法与其操作的数据类型分离。举个例子一个排序算法无论是给整数排序、给浮点数排序还是给字符串排序其核心逻辑如快速排序的分治思想是完全一致的。用面向对象实现我们可能需要为每种数据类型定义一个包含sort方法的类这导致了逻辑重复。而泛型编程则允许我们只写一套算法代码让它适用于任何符合该算法要求的类型。两者关系它们不是对立的而是相辅相成的。一个设计良好的C程序通常会同时使用两者。例如STL中的std::vectorT是一个类模板泛型但它可以存储任何类型的对象包括用户自定义的类对象并且这些对象可以很好地利用OOP特性。2.2 函数模板通用算法的蓝图函数模板的本质是定义一个函数家族这些函数除了数据类型不同其他逻辑完全相同。它使用一个或多个类型参数通常用typename T或class T表示作为占位符。为什么有效编译器在编译期间根据你调用函数时传入的实参类型推导出模板参数T的具体类型然后实例化Instantiate出一份该特定类型的函数代码。这个过程叫做模板实例化。它是在编译期完成的因此不会带来任何运行时开销生成的代码效率和手写的一样高。一个关键心智模型不要把函数模板看作一个可以直接调用的函数而应视作一个函数工厂。你提供“类型”作为原材料编译器这个工厂为你生产出对应的具体函数。2.3 类模板通用数据结构的模具如果说函数模板是生产通用算法的工厂那么类模板就是生产通用数据结构或容器的模具。最经典的例子就是STL中的vector、list、map。类模板允许我们将类定义中的某些成员变量的类型、成员函数的参数或返回类型“参数化”。这样我们就可以用同一套类代码创建出存储int的向量、存储string的向量甚至是存储自定义Student对象的向量。与函数模板的关键区别函数模板的类型参数通常可以由编译器自动推导而类模板的类型参数必须在创建对象时显式指定。因为编译器需要知道具体的类型才能为这个类分配内存、生成方法。2.4 仿函数函数对象行为抽象的桥梁仿函数Functor顾名思义就是“模仿函数”的对象。它是一个重载了函数调用运算符()的类或结构体的对象。为什么它在模板和泛型编程中如此重要状态保持普通的函数指针只能指向一个函数地址而仿函数作为一个对象可以拥有自己的成员变量从而在多次调用间保持状态。这是函数指针无法做到的。内联优化仿函数的operator()可以被编译器内联而通过函数指针调用函数通常难以内联这在性能敏感的泛型算法如STL的sort中至关重要。与模板无缝集成STL算法如std::sort,std::for_each通常接受一个“可调用对象”作为谓词Predicate或比较器。这个“可调用对象”可以是函数指针、lambda表达式也可以是仿函数。仿函数因其灵活性和效率成为最常用的形式之一。仿函数完美地体现了OOP与泛型编程的结合它本身是一个类OOP但其实例可以像函数一样被调用并作为参数传递给泛型算法。3. 函数模板深度实战从编写到优化理解了理论我们立刻动手把函数模板的每一个细节掰开揉碎。3.1 基础语法与编译期实例化让我们从一个最简单的“求最大值”函数开始。// 函数模板声明与定义 template typename T // 模板参数列表声明一个类型参数T T myMax(T a, T b) { // T 被用作参数类型和返回类型 return (a b) ? a : b; } int main() { int i1 10, i2 20; double d1 3.14, d2 2.71; std::string s1 hello, s2 world; // 编译器自动推导类型并实例化 std::cout myMax(i1, i2) std::endl; // 实例化 myMaxint(int, int) std::cout myMax(d1, d2) std::endl; // 实例化 myMaxdouble(double, double) // std::cout myMax(s1, s2) std::endl; // 实例化 myMaxstd::string(...) 前提是std::string定义了运算符 return 0; }关键点解析template typename T也可以用template class T在模板参数声明中两者含义完全相同都表示“一个类型参数”。typename更现代避免了与类声明的class混淆。当调用myMax(i1, i2)时编译器看到实参是int于是将模板中所有的T替换为int生成一份int myMax(int a, int b)的代码。这个过程是静默发生的你可以通过编译器生成的中间文件如GCC的-fdump-tree-original来查看实例化后的代码。注意myMax(s1, s2)能编译的前提是std::string重载了operator用于比较字典序。模板本身不关心类型T是什么但它要求你对T的操作这里是是合法的。这就是所谓的“隐式接口”或“概念”C20前。3.2 多参数与默认模板参数函数模板可以有多个类型参数也可以为类型参数指定默认值。// 多类型参数 template typename T1, typename T2 auto addMixed(const T1 a, const T2 b) - decltype(a b) { // 使用auto和尾置返回类型推导结果类型 return a b; } // 默认模板参数 (C11起) template typename T int // 默认T为int T defaultValueFunc() { return T{}; // 返回T类型的默认初始化值对于int是0 } int main() { auto sum1 addMixed(5, 3.14); // T1int, T2double, 返回类型double auto val defaultValueFunc(); // 使用默认int auto val2 defaultValueFuncdouble(); // 显式指定double }实操心得当涉及多个不同类型参数运算时如T1 T2返回类型可能不容易直接写出。decltype和C14的auto返回类型推导是解决此问题的利器。默认模板参数在编写通用库时非常有用可以为用户提供便利同时保留灵活性。3.3 模板参数推导的陷阱与显式指定大多数时候编译器推导得很好。但有些情况需要你出手干预。template typename T void printContainer(const T container) { for (const auto elem : container) { std::cout elem ; } std::cout std::endl; } template typename T T* create() { return new T(); } int main() { std::vectorint vec {1, 2, 3}; printContainer(vec); // 正确T被推导为std::vectorint // 情况1无法推导 // printContainer(std::vector{1,2,3}); // C17前错误无法推导元素类型。C17的类模板参数推导(CTAD)可以解决。 printContainerstd::vectorint(std::vector{1,2,3}); // 显式指定T // 情况2希望返回特定类型指针 auto ptr createint(); // 必须显式指定编译器无法从空参数列表推导T delete ptr; }注意事项当模板参数没有出现在函数参数列表中时如create必须显式指定。当有多个重载或特化版本编译器推导可能产生歧义时显式指定可以消除歧义。显式指定的语法是在函数名后加尖括号function_nameType(arguments)。3.4 重载决议模板与非模板的竞争当存在同名的普通函数和函数模板时编译器如何选择// 普通函数 void myPrint(int x) { std::cout 普通函数: x std::endl; } // 函数模板 template typename T void myPrint(T x) { std::cout 函数模板: x std::endl; } int main() { myPrint(10); // 调用哪个 myPrint(10.0); // 调用哪个 myPrint(a); // 调用哪个 }重载决议规则简化版精确匹配优先如果普通函数的参数类型与实参类型完全匹配则优先选择普通函数。因此myPrint(10)调用普通函数。模板匹配如果没有精确匹配的普通函数但模板实例化后可以匹配则选择模板。因此myPrint(10.0)double和myPrint(a)char都调用模板实例化的版本。转型匹配如果普通函数可以通过隐式类型转换如double转int匹配而模板可以精确匹配则模板优先。因为转换需要代价。但这条规则比较复杂有时依赖于编译器实现。避坑指南在实际项目中避免设计这种容易引起混淆的重载。如果必须同时提供确保它们有清晰、不同的语义或者使用不同的函数名。4. 类模板全面剖析构建你自己的Vector理解了函数模板类模板就顺理成章了。我们通过实现一个简化版的MyVector来掌握所有要点。4.1 类模板的定义与成员函数实现类模板的声明和定义通常放在同一个头文件.hpp中。这是因为模板代码在编译期需要被看到全部定义才能实例化。// MyVector.hpp #ifndef MY_VECTOR_HPP #define MY_VECTOR_HPP #include cstddef // for size_t #include algorithm // for std::copy template typename T // 类模板声明 class MyVector { private: T* m_data; // 指向动态数组的指针 size_t m_size; // 当前元素数量 size_t m_capacity; // 当前分配的内存容量 public: // 1. 构造函数 explicit MyVector(size_t initCapacity 10) // 防止隐式转换 : m_data(new T[initCapacity]), m_size(0), m_capacity(initCapacity) {} // 2. 析构函数 ~MyVector() { delete[] m_data; } // 3. 拷贝构造函数深拷贝 MyVector(const MyVector other) : m_data(new T[other.m_capacity]), m_size(other.m_size), m_capacity(other.m_capacity) { std::copy(other.m_data, other.m_data other.m_size, m_data); } // 4. 拷贝赋值运算符 MyVector operator(const MyVector other) { if (this ! other) { // 防止自赋值 // 经典“拷贝并交换” idiom MyVector temp(other); // 拷贝构造临时对象 swap(temp); // 交换*this和temp的内容 } // temp析构释放原资源 return *this; } // 5. 移动构造函数 (C11) MyVector(MyVector other) noexcept : m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) { other.m_data nullptr; other.m_size other.m_capacity 0; } // 交换辅助函数 void swap(MyVector other) noexcept { using std::swap; swap(m_data, other.m_data); swap(m_size, other.m_size); swap(m_capacity, other.m_capacity); } // 元素访问 T operator[](size_t index) { // 实际项目中应添加边界检查 return m_data[index]; } const T operator[](size_t index) const { return m_data[index]; } // 容量操作 size_t size() const { return m_size; } size_t capacity() const { return m_capacity; } bool empty() const { return m_size 0; } // 添加元素 void push_back(const T value) { if (m_size m_capacity) { reserve(m_capacity 0 ? 1 : m_capacity * 2); // 扩容策略 } m_data[m_size] value; // 在尾部构造新元素 } void push_back(T value) { // 移动语义重载 (C11) if (m_size m_capacity) { reserve(m_capacity 0 ? 1 : m_capacity * 2); } m_data[m_size] std::move(value); // 移动构造 } // 内存管理 void reserve(size_t newCapacity) { if (newCapacity m_capacity) return; T* newData new T[newCapacity]; std::copy(m_data, m_data m_size, newData); delete[] m_data; m_data newData; m_capacity newCapacity; } // ... 其他成员函数如 pop_back, clear, insert, erase 等 }; #endif // MY_VECTOR_HPP核心要点拆解资源管理这是类模板设计的核心。我们手动管理m_data指向的堆内存遵循RAII原则构造函数获取资源析构函数释放资源。拷贝控制成员拷贝构造、拷贝赋值、析构必须正确实现防止内存泄漏和重复释放。模板参数T的使用T作为元素类型出现在成员变量T* m_data、函数参数const T value、返回类型T operator[]中。这意味着MyVector可以存储任何类型T的对象只要T满足一些基本要求如可拷贝构造、可析构。成员函数定义在类模板内部定义的成员函数默认为内联函数。如果在类外定义语法比较特殊template typename T void MyVectorT::push_back(const T value) { /* 实现 */ }异常安全注意reserve函数中的操作顺序。先分配新内存并拷贝成功再释放旧内存。这保证了即使new或std::copy抛出异常原容器的状态也不会被破坏强异常安全保证。4.2 类模板的实例化与使用使用类模板时必须显式提供模板参数。#include MyVector.hpp #include string int main() { // 实例化 MyVectorint MyVectorint intVec; intVec.push_back(1); intVec.push_back(2); std::cout intVec[0] std::endl; // 使用重载的operator[] // 实例化 MyVectorstd::string MyVectorstd::string strVec; strVec.push_back(Hello); strVec.push_back(Template); // strVec[0] 返回 std::string可以调用其成员函数 std::cout strVec[0].size() std::endl; // 使用拷贝构造函数 MyVectorint copiedVec intVec; // 调用 MyVectorint 的拷贝构造函数 // 错误示例未提供模板参数 // MyVector vec; // 错误C17前类模板参数必须显式指定。 // C17 类模板参数推导(CTAD)允许MyVector deducedVec{1,2,3}; // 推导为MyVectorint return 0; }重要提示MyVectorint和MyVectorstd::string是两个完全不同的类。编译器会为它们分别生成代码。这被称为代码膨胀是模板的一个潜在缺点需要权衡。4.3 类模板的特化与偏特化有时对于特定的类型通用的模板实现可能效率低下甚至无法工作。这时就需要模板特化。全特化为某个特定的类型提供完全不同的实现。// 通用模板 template typename T class MyTypeInfo { public: static const char* name() { return Unknown Type; } }; // 全特化版本 for int template class MyTypeInfoint { public: static const char* name() { return int; } }; // 全特化版本 for double template class MyTypeInfodouble { public: static const char* name() { return double; } }; int main() { std::cout MyTypeInfochar::name() std::endl; // 输出: Unknown Type std::cout MyTypeInfoint::name() std::endl; // 输出: int }偏特化为某一类类型如指针类型、特定模板的实例等提供特殊实现。// 通用模板 template typename T class MyPointerWrapper { // 通用实现 }; // 偏特化对所有指针类型 template typename T class MyPointerWrapperT* { // 针对指针的特殊实现例如可以自动解引用等 }; // 偏特化对特定模板实例如 MyVectorT template typename T class MySpecialHandlerMyVectorT { // 针对MyVector容器的特殊处理 };特化是高级模板技术在元编程和库设计中广泛应用它让模板具备了类似“条件分支”的能力。5. 仿函数函数对象高级应用仿函数不仅仅是重载了()的类它在STL和泛型编程中扮演着策略Policy和适配器的角色。5.1 仿函数作为算法策略STL算法如std::sort、std::transform、std::accumulate都接受仿函数作为自定义操作。#include vector #include algorithm #include iostream // 1. 普通仿函数比较器 struct CompareByLength { bool operator()(const std::string a, const std::string b) const { return a.size() b.size(); } }; // 2. 带状态的仿函数计数器 class CountGreaterThan { int threshold; mutable int count; // mutable 允许在const成员函数中修改 public: explicit CountGreaterThan(int t) : threshold(t), count(0) {} bool operator()(int value) const { if (value threshold) { count; return true; } return false; } int getCount() const { return count; } }; // 3. 仿函数作为运算器 template typename T struct Multiplier { T factor; explicit Multiplier(const T f) : factor(f) {} T operator()(const T x) const { return x * factor; } }; int main() { std::vectorstd::string words {apple, banana, cherry, date}; std::vectorint numbers {5, 12, 3, 20, 7, 15}; // 使用仿函数排序 std::sort(words.begin(), words.end(), CompareByLength()); for (const auto w : words) std::cout w ; // 输出: date apple cherry banana // 使用带状态的仿函数 CountGreaterThan counter(10); auto it std::remove_if(numbers.begin(), numbers.end(), std::ref(counter)); // 注意用std::ref传递引用 numbers.erase(it, numbers.end()); std::cout \nRemoved counter.getCount() numbers greater than 10. std::endl; // 使用仿函数进行变换 std::vectorint vec {1, 2, 3, 4}; Multiplierint timesTwo(2); std::transform(vec.begin(), vec.end(), vec.begin(), timesTwo); // vec 变为 {2, 4, 6, 8} }关键技巧当算法需要拷贝仿函数时如std::sort使用临时对象CompareByLength()即可。当算法以值传递方式接受谓词而你又希望保留仿函数内部状态时如std::remove_if必须使用std::ref或std::cref将仿函数包装成引用传递否则算法内部操作的是仿函数的副本外部无法获取修改后的状态。模板化的仿函数如Multiplier可以适用于多种类型更加通用。5.2 标准库中的仿函数functionalC标准库在functional头文件中定义了许多有用的仿函数它们通常继承自std::unary_function或std::binary_functionC17已弃用但概念仍在。#include functional #include algorithm #include vector int main() { std::vectorint vec {5, 3, 8, 1, 9}; // 算术仿函数 std::plusint add; int sum std::accumulate(vec.begin(), vec.end(), 0, add); // 等价于默认的加法 std::multipliesint mult; // 关系仿函数 std::greaterint gt; std::sort(vec.begin(), vec.end(), gt); // 降序排序 // 逻辑仿函数 std::vectorbool flags {true, false, true}; std::vectorbool results; std::transform(flags.begin(), flags.end(), std::back_inserter(results), std::logical_notbool()); // 取反 // 适配器将二元函数转换为一元函数 std::vectorint vec2 {10, 20, 30}; std::vectorint result(vec2.size()); auto minus10 std::bind(std::minusint(), std::placeholders::_1, 10); // 创建一个“减去10”的一元函数 std::transform(vec2.begin(), vec2.end(), result.begin(), minus10); // result: {0, 10, 20} }经验之谈在C11之后Lambda表达式在很多场景下已经取代了简单的仿函数因为它写起来更简洁。例如std::sort(vec.begin(), vec.end(), std::greaterint())完全可以写成std::sort(vec.begin(), vec.end(), [](int a, int b){ return a b; })。但对于需要复杂状态、或需要作为类型参数传递如模板模板参数的场景定义明确的仿函数类仍然不可替代。6. 模板实战中的高级议题与避坑指南掌握了基础我们来看看实际项目中会遇到哪些挑战。6.1 分离编译问题与解决方案这是模板新手最常见的“坑”。尝试将类模板的声明和实现分离到.h和.cpp文件会导致链接错误。// MyTemplate.h templatetypename T class MyTemplate { public: void doSomething(const T t); }; // MyTemplate.cpp #include MyTemplate.h templatetypename T void MyTemplateT::doSomething(const T t) { // 实现 } // main.cpp #include MyTemplate.h int main() { MyTemplateint obj; obj.doSomething(5); // 链接错误undefined reference }原因模板是编译期生成代码的蓝图。当编译器编译main.cpp时它看到了MyTemplateint的声明但找不到MyTemplateint::doSomething的定义因为它在.cpp文件里而该.cpp文件没有被实例化int版本。编译器不会去MyTemplate.cpp里寻找链接器自然也找不到。解决方案最常用将实现也放在头文件中这是STL和大多数库的做法。将成员函数的定义直接写在类内或者写在头文件的类定义之后。显式实例化在.cpp文件的末尾显式告诉编译器你需要哪些类型的实例。// MyTemplate.cpp #include MyTemplate.h templatetypename T void MyTemplateT::doSomething(const T t) { /* 实现 */ } // 显式实例化你需要的类型 template class MyTemplateint; template class MyTemplatedouble;这样做的缺点是你必须预先知道所有会用到的类型失去了部分泛型灵活性。使用export关键字已弃用C98曾引入export试图解决此问题但实现复杂且支持有限在C11中已被弃用C17中移除。6.2 类型约束与SFINAE模板对类型T的操作是“鸭子类型”的只要T能进行模板中要求的操作如operator就可以实例化。但有时我们需要对T施加更明确的约束。在C20之前常用SFINAESubstitution Failure Is Not An Error替换失败并非错误技术来约束模板。#include type_traits // 使用SFINAE只有T是算术类型int, double等时这个模板才参与重载 templatetypename T typename std::enable_ifstd::is_arithmeticT::value, T::type arithmeticMax(T a, T b) { return (a b) ? a : b; } // 对于非算术类型上面的模板实例化会失败SFINAE编译器会选择其他可能的重载而不是报错。 // 如果没有其他重载则最终编译错误。 // C20 概念Concepts提供了更优雅的解决方案 templatetypename T concept Arithmetic std::is_arithmetic_vT; templateArithmetic T // 使用概念约束T T conceptMax(T a, T b) { return (a b) ? a : b; }建议如果你的项目使用C20或更高标准优先使用概念。它让模板的接口约束变得清晰易懂错误信息也更友好。SFINAE虽然强大但语法晦涩是模板元编程的高级技巧容易出错。6.3 模板与动态多态虚函数的结合模板是编译期多态虚函数是运行期多态。它们可以结合使用创造出灵活的设计。// 一个抽象基类定义接口 class Drawable { public: virtual ~Drawable() default; virtual void draw() const 0; }; // 一个类模板实现这个接口可以包装任何可绘制的类型T templatetypename T class DrawableWrapper : public Drawable { T wrappedObj; public: explicit DrawableWrapper(T obj) : wrappedObj(std::move(obj)) {} void draw() const override { // 要求类型T有一个名为draw的成员函数或者有全局的draw(T)函数。 // 这里假设是成员函数。 wrappedObj.draw(); } }; // 使用 class Circle { public: void draw() const { std::cout Drawing Circle\n; } }; class Square { public: void draw() const { std::cout Drawing Square\n; } }; int main() { std::vectorstd::unique_ptrDrawable shapes; shapes.push_back(std::make_uniqueDrawableWrapperCircle(Circle{})); shapes.push_back(std::make_uniqueDrawableWrapperSquare(Square{})); for (const auto shape : shapes) { shape-draw(); // 运行时多态调用 } }这种模式被称为类型擦除Type Erasurestd::function和std::any内部就使用了类似的技术。它结合了模板的灵活性和虚函数的统一接口。6.4 性能考量代码膨胀与内联代码膨胀模板会为每一种用到的类型组合生成一份代码。MyVectorint,MyVectordouble,MyVectorstd::string会产生三份几乎相同的机器码。如果模板代码很大如复杂的排序算法这会导致最终可执行文件体积显著增大。缓解策略将模板代码中与类型无关的部分抽取到非模板的辅助函数或基类中。谨慎实例化避免在不必要的地方使用模板。对于指针类型考虑使用特化或使用类型擦除技术如void*加函数指针但会损失类型安全。内联优势模板函数/成员函数通常定义在头文件中且很简单这给了编译器极大的内联优化机会。像std::sort中传入的比较器仿函数其operator()调用很可能被内联消除了函数调用的开销这是模板泛型算法高性能的重要原因之一。7. 从理论到实践一个综合案例——通用缓存类最后我们设计一个简单的通用缓存类LRUCache最近最少使用缓存综合运用类模板、仿函数等知识。#include unordered_map #include list #include functional // for std::function template typename KeyT, typename ValueT class LRUCache { private: using ListIter typename std::liststd::pairKeyT, ValueT::iterator; size_t m_capacity; std::liststd::pairKeyT, ValueT m_cacheList; // 双向链表维护访问顺序最近访问的在链表头 std::unordered_mapKeyT, ListIter m_cacheMap; // 哈希表提供O(1)查找 // 一个可选的“未命中时加载数据”的策略仿函数 std::functionValueT(const KeyT) m_missLoader; public: // 构造函数接受容量和可选的加载器 explicit LRUCache(size_t capacity, std::functionValueT(const KeyT) missLoader nullptr) : m_capacity(capacity), m_missLoader(std::move(missLoader)) {} // 获取值。如果存在将其移到链表头部并返回如果不存在尝试加载。 ValueT get(const KeyT key) { auto mapIt m_cacheMap.find(key); if (mapIt m_cacheMap.end()) { // 缓存未命中 if (m_missLoader) { ValueT value m_missLoader(key); put(key, value); // 加载后放入缓存 return value; } else { throw std::range_error(Key not found and no loader provided.); } } // 缓存命中 // 1. 将命中的节点移动到链表头部 m_cacheList.splice(m_cacheList.begin(), m_cacheList, mapIt-second); // 2. 返回对应的值 return mapIt-second-second; } // 插入或更新键值对 void put(const KeyT key, const ValueT value) { auto mapIt m_cacheMap.find(key); if (mapIt ! m_cacheMap.end()) { // 键已存在更新值并移到头部 mapIt-second-second value; m_cacheList.splice(m_cacheList.begin(), m_cacheList, mapIt-second); return; } // 键不存在需要插入 if (m_cacheMap.size() m_capacity) { // 容量已满淘汰链表尾部的节点最久未使用 auto lastIter std::prev(m_cacheList.end()); m_cacheMap.erase(lastIter-first); m_cacheList.pop_back(); } // 在链表头部插入新节点 m_cacheList.emplace_front(key, value); m_cacheMap[key] m_cacheList.begin(); } size_t size() const { return m_cacheMap.size(); } bool contains(const KeyT key) const { return m_cacheMap.find(key) ! m_cacheMap.end(); } void clear() { m_cacheList.clear(); m_cacheMap.clear(); } }; // 使用示例 int main() { // 创建一个容量为2的缓存未命中时返回-1 LRUCacheint, std::string cache(2); cache.put(1, Data1); cache.put(2, Data2); std::cout cache.get(1) std::endl; // 输出 Data1 此时 (1,Data1) 变为最近使用 cache.put(3, Data3); // 容量已满会淘汰最久未使用的 (2,Data2) std::cout cache.contains(2) std::endl; // 输出 0 (false) // 使用加载器的缓存 auto loader [](int key) - std::string { std::cout Loading data for key: key std::endl; return LoadedData std::to_string(key); }; LRUCacheint, std::string cacheWithLoader(3, loader); auto data cacheWithLoader.get(100); // 输出 Loading data for key: 100并缓存 std::cout data std::endl; // 输出 LoadedData100 auto cachedData cacheWithLoader.get(100); // 直接从缓存获取无输出 std::cout cachedData std::endl; // 输出 LoadedData100 }设计要点回顾模板化KeyT和ValueT使得缓存可以存储任意类型的键值对。数据结构选择结合std::list维护顺序和std::unordered_map快速查找实现O(1)的get和put操作。策略模式通过std::function接受一个“缓存未命中加载器”将数据加载逻辑与缓存逻辑解耦提高了类的复用性。移动语义在put函数中我们使用了value的拷贝。在实际应用中可以考虑添加右值引用版本void put(KeyT key, ValueT value)来支持移动构造提升性能。异常安全put函数在插入新元素前先执行淘汰操作保证了即使后续插入失败如内存不足缓存的状态也是一致的。这个案例展示了如何将面向对象的设计原则单一职责、开闭原则与泛型编程的强大表达能力结合起来构建出一个既通用又高效的组件。模板不是银弹但它给了我们塑造代码、适应多变需求的强大工具。当你下次使用std::vector或std::sort时不妨想想其背后模板与仿函数精妙协作的世界这会让你的C之旅更加通透。
返回列表