C++代理模式进阶:从虚代理到智能指针与多线程并发控制

发布时间:2026/9/9 15:08:55
C++代理模式进阶:从虚代理到智能指针与多线程并发控制 1. 先聊明白代理模式在C里到底解决什么问题1.1 核心结构与最直白的理解方式代理模式Proxy Pattern在C面试和实际工程里都算得上高频角色。很多人把它背成一个类包装另一个类但真正落到代码层面C的代理模式跟Java、C#里的写法有挺大差别这跟语言本身的资源管理方式、多态实现机制、以及C开发者对性能近乎偏执的追求都有关系。我习惯用一个很生活化的类比来讲代理模式你不想直接跟房东打交道所以找了中介。中介和房东都提供租房这个能力但中介在中间加了筛选租客、代看房、签合同等额外动作。在代码里房东是真实主题RealSubject中介是代理Proxy而租房这个动作就是抽象接口Subject。经典结构是三个角色一个抽象接口、一个真实主题类、一个代理类。代理类持有真实主题的指针或引用并且实现了同样的接口。调用方只跟代理交互代理在转发调用给真实主题之前或之后插入自己的逻辑。在C里有个很有意思的点代理模式经常跟智能指针长得很像但本质上不是一回事。智能指针管的是生命周期代理管的是行为控制。当然高级用法里两者可以叠加后面我会讲到怎么用unique_ptr和shared_ptr来做带资源管理的代理那是普通八股文里不会教的东西。1.2 为什么C场景下需要高级代理简单代理大家都会写但真实项目里很少遇到那么规整的需求。C里高级代理主要解决这几类问题惰性加载对象的构造代价很高比如加载大图、初始化模型、连接数据库你希望真正用到时才构造而不是程序启动时就白白浪费资源。访问控制某些操作只允许特定角色调用代理在转发前做权限校验。日志审计希望记录每次调用的时间、参数、结果但不想把日志逻辑侵入到业务类里。远程调用调用远端服务时代理负责序列化、网络传输、反序列化调用方感觉就像在调本地函数。缓存复用重复查询相同结果时代理直接返回缓存避免重复计算或IO。并发保护多个线程同时访问共享对象时代理统一加锁避免把锁散落在业务代码各处。这些场景单独拎出来都不算难难点在于把它们组合进一个已有的C项目里还要兼顾性能、异常安全和代码可维护性。C不像Java有现成的动态代理库Java的java.lang.reflect.Proxy可以运行时生成代理类C没有这种运行时反射能力所以C的代理模式更依赖模板、虚函数和RAII这些语言特性来手工打造。另外一个很现实的原因是C面试里代理模式经常被用来考察候选人对多态、虚析构、对象切片、智能指针的理解深度。你光会画UML图是不够的面试官会追着问代理对象持有什么类型的指针虚析构没写会怎样如果真实对象在代理之前就被销毁了怎么办。这些问题只有真正写过代理的人才能答得干净。2. 进阶代理形态虚代理、保护代理与日志代理2.1 虚代理把昂贵创建延后到最后一刻虚代理Virtual Proxy是延迟初始化的经典实现。核心思路是代理对象一开始不创建真实对象等第一次调用接口方法时才真正构造。我在一个图像处理项目里用过这个模式。程序启动时要读一堆大分辨率图片的元信息但真正要解码像素的时刻往往在用户点击之后。如果启动时就把所有图片全部解码内存直接爆掉如果每次用到都现场解码响应又太慢。虚代理的做法是class Image { public: virtual ~Image() default; virtual void render() 0; }; class HighResImage : public Image { public: explicit HighResImage(const std::string path) : path_(path) { // 模拟昂贵的解码操作 loadFromDisk(); } void render() override { std::cout 渲染图片: path_ std::endl; } // 为了展示把加载过程拆开 void loadFromDisk() { std::cout 正在从磁盘解码: path_ (耗时操作) std::endl; } private: std::string path_; }; class ImageProxy : public Image { public: explicit ImageProxy(std::string path) : path_(std::move(path)), realImage_(nullptr) {} ~ImageProxy() override { delete realImage_; } void render() override { // 第一次调用时才真正构造真实对象 if (!realImage_) { realImage_ new HighResImage(path_); } realImage_-render(); } private: std::string path_; HighResImage* realImage_; };这段代码有几个关键细节需要解释裸指针加手工delete这里为了讲清楚虚代理的原理先用裸指针演示。真实项目中强烈建议用unique_ptr替换避免异常安全问题和内存泄漏。代理析构时销毁真实对象代理负责真实对象的生命周期所以析构函数必须delete。如果忘了写虚析构通过Image*删除代理对象时行为是未定义的这又是一个经典的C坑。线程安全问题上面这个实现只在单线程下安全。如果两个线程同时第一次调用render会重复创建真实对象甚至出现内存泄漏。后面我会专门讲线程安全的代理写法。虚代理的优势在于把昂贵对象的创建时机从程序运行早期推迟到实际使用点对启动性能优化特别明显。但注意如果所有代理最终都会被调用那虚代理纯属多此一举反而多了一层间接调用开销。2.2 保护代理权限控制与安全检查的执行者保护代理Protection Proxy在转发调用前做权限检查。它适合用在管理后台、系统设置、敏感数据访问等场景。我在写一个简单的权限管理系统时用过保护代理。需求是普通用户只能读数据管理员才能删数据。一种做法是在业务类里加if判断但这样业务类会越写越臃肿职责越来越多。另一种做法是把权限检验抽到代理层业务类只管干活。enum class Role { Guest, User, Admin }; class DataService { public: virtual ~DataService() default; virtual void readData() 0; virtual void deleteData() 0; }; class RealDataService : public DataService { public: void readData() override { std::cout 读取数据成功 std::endl; } void deleteData() override { std::cout 删除数据成功 std::endl; } }; class DataServiceProxy : public DataService { public: DataServiceProxy(Role role, DataService* service) : currentRole_(role), realService_(service) {} void readData() override { // 读操作所有角色都能执行 realService_-readData(); } void deleteData() override { if (currentRole_ ! Role::Admin) { throw std::runtime_error(权限不足: 只有管理员可以删除数据); } realService_-deleteData(); } private: Role currentRole_; DataService* realService_; };这种写法的好处非常直观权限逻辑集中管理不散落在业务代码里。如果以后权限规则变复杂了比如要支持细粒度的字段级权限可以直接在代理里扩展不用改动真实业务类。不过保护代理有几个容易踩的点真实对象指针的生命周期如果代理不拥有真实对象那真实对象的创建和销毁必须由外部保证否则悬垂指针会带来噩梦级bug。异常安全权限检查失败的抛出异常调用方需要捕获。如果真实服务已经执行到一半异常的传播路径需要谨慎设计。职责边界保护代理管的是能不能调用而不是怎么调用。如果代理还要负责参数校验、结果格式化那它就不是代理变成了门面Facade或装饰器Decorator职责会混乱。2.3 日志代理与远程代理审计和分布式的基础设施日志代理Logging Proxy的用途很纯粹在转发调用前后记录审计信息。它比较像AOP面向切面编程但C没有原生的AOP支持用代理来实现是很务实的方案。class LoggerProxy : public DataService { public: LoggerProxy(DataService* service) : realService_(service) {} void readData() override { auto start std::chrono::high_resolution_clock::now(); realService_-readData(); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout [日志] readData 耗时: duration.count() us std::endl; } void deleteData() override { std::cout [日志] deleteData 被调用, 调用者: caller_ std::endl; realService_-deleteData(); std::cout [日志] deleteData 完成 std::endl; } private: DataService* realService_; std::string caller_; };远程代理Remote Proxy则是分布式系统里的重要组件。它的核心工作是屏蔽网络通信细节让调用方觉得本地对象一样。代理内部做序列化、发送请求、等待响应、反序列化、返回结果。C里常见的场景包括gRPC客户端、数据库驱动、消息队列客户端等。有些SDK本身就内置了代理模式的影子里比如UG二次开发中的Block UI对话框操作封装OpenCV的某些高层接口也类似你在上层调的接口背后可能就封装了复杂的底层调用链。远程代理要特别注意性能开销。一次函数调用从本地变成网络请求延迟可能从纳秒级放大到毫秒甚至秒级。所以远程代理通常还会配合连接池、超时重试、异步回调等机制。在C里远程代理往往用智能指针和异步任务std::async、std::future来组合实现。3. 高级接口设计与C特有的实现技巧3.1 智能指针与RAII加持的代理把代理模式和智能指针结合起来是我在实际项目里最推荐的做法。它解决了一个核心痛点代理对象和真实对象的生命周期管理。先看一个反面例子。很多初学者写的代理是这样class Proxy { public: Proxy(RealSubject* real) : real_(real) {} ~Proxy() { delete real_; } private: RealSubject* real_; };问题在于代理可能被拷贝拷贝后两个代理指向同一个真实对象析构时就会double free。解决方法是禁用拷贝、使用引用计数、或者用移动语义。但这些手工实现容易出错不如直接用标准库的shared_ptr。用shared_ptr做生命周期的代理可以写成这样class SharedProxy : public Subject { public: explicit SharedProxy(std::shared_ptrRealSubject real) : real_(std::move(real)) {} void operation() override { // 代理逻辑 real_-operation(); // 代理逻辑 } private: std::shared_ptrRealSubject real_; };外部使用时只需要保证真实对象以shared_ptr形式创建auto real std::make_sharedRealSubject(); auto proxy std::make_sharedSharedProxy(real);这样代理和真实对象的生命周期由shared_ptr统一管理不会出现悬垂指针也不会double free。还有一个进阶玩法unique_ptr配合自定义删除器。比如真实对象来自某个不提供析构函数的C风格SDK你需要用特定的释放函数。自定义删除器可以把这个释放逻辑封装在代理内部。auto deleter [](RealSubject* p) { std::cout 使用自定义删除器释放资源 std::endl; p-cleanup(); delete p; }; std::unique_ptrRealSubject, decltype(deleter) real(new RealSubject(), deleter);在代理里再包装一层unique_ptr能实现代理销毁时真实对象自动销毁的效果而且不会误删。但这个方案要求理解std::unique_ptr的移动语义代理类通常也要声明为move-only。3.2 模板代理不做虚函数也能实现多态很多C程序员对代理的直觉是代理必须继承抽象基类、必须重写虚函数。但C除了运行时多态还有编译期多态。模板代理就是利用编译期多态实现代理的一种高级手法。模板代理的核心思路是代理类是一个模板模板参数是真实对象的类型。代理不继承任何抽象接口而是通过成员函数名匹配来实现相同的调用方式。template typename Real class TemplateProxy { public: explicit TemplateProxy(Real real) : real_(real) {} template typename... Args auto call(Args... args) { // 代理前置逻辑 std::cout [模板代理] 调用前置逻辑 std::endl; // 转发给真实对象 auto result real_.operation(std::forwardArgs(args)...); // 代理后置逻辑 std::cout [模板代理] 调用后置逻辑 std::endl; return result; } private: Real real_; };之所以说它是代理是因为它同样控制了对真实对象的访问同样可以在前后插入逻辑。但它的实现完全不依赖虚函数所以没有虚函数调用的开销也没有虚表的内存占用。在性能敏感的嵌入式或高频交易系统里这种无虚函数开销的代理很有价值。不过要注意适用范围模板代理是编译期绑定的也就是说你在编译时就知道真实对象的具体类型。业务代码必须直接持有真实对象不能通过基类指针传入。如果需要在运行时决定用哪个真实对象比如从配置文件读取类型那还是得回到虚函数方案。我的经验是需要组合多种代理行为时模板代理搭配std::invoke和std::forward可以做得很优雅而且不容易出现虚函数方案里的对称性问题。但如果团队成员水平参差不齐维护成本会偏高因为模板错误信息读起来很痛苦。3.3 多线程安全代理把并发控制收进代理层C多线程环境下的代理最常见的需求是让真实对象的方法调用变成线程安全。这比单纯给每个方法加锁要复杂因为要考虑锁的粒度、死锁、以及异常情况下锁的正确释放。先看一个简单的加锁代理#include mutex #include shared_mutex class ThreadSafeProxy : public Subject { public: explicit ThreadSafeProxy(std::shared_ptrRealSubject real) : real_(std::move(real)) {} void readOperation() override { // 读操作共享锁 std::shared_lockstd::shared_mutex lock(mutex_); real_-readOperation(); } void writeOperation() override { // 写操作独占锁 std::unique_lockstd::shared_mutex lock(mutex_); real_-writeOperation(); } private: std::shared_ptrRealSubject real_; mutable std::shared_mutex mutex_; };这里用了std::shared_mutexC17才支持读写锁可以让多个读者并发访问但写者独占比单纯的std::mutex效率高不少。如果线上环境还停留在C14可以用std::shared_timed_mutex替代。但要注意这种每个方法独立加锁的代理有可能踩坑。举个例子真实对象有两个方法stepA()和stepB()业务逻辑要求它们必须原子执行即执行A期间不允许别的线程执行B。如果代理只是分别给A和B加锁那别的线程可能插入在A和B之间。这就引出一个经典问题锁粒度太细组合操作不是原子的。解决方案有几种在代理层提供executeInLock(callback)接口让外部传入一个lambda代理在外层统一加锁lambda内部可以安全执行多个步骤。或者把真实对象的接口设计成粗粒度一次性完成组合操作。或者用递归锁std::recursive_mutex让同一线程可以重复加锁避免组合操作里出现死锁。再提一个细节析构和加锁的关系。如果代理在持有锁的情况下析构真实对象而另一个线程正在等待这把锁那就会有线程永远等不到锁程序挂死。正确做法是析构时先释放引用让真实对象的生命周期由shared_ptr控制锁只保护方法调用。这就是为什么我推荐用shared_ptr持有真实对象而不是在代理里裸持有。3.4 C17/20新特性给代理带来的可能性新标准给代理模式带来了一些新工具std::jthreadC20引入的线程类析构时自动join不会因为异常导致std::terminate。用在异步代理里很省心。std::lazyC20提案延迟计算的类型可以跟虚代理结合得更优雅。不过这个特性还没完全合入标准库暂时还是靠手写lambda和std::optional实现。std::expectedC23的预期值类型做错误处理比异常更轻量非常适合用在代理的转发逻辑中。比如保护代理权限校验失败可以返回std::unexpected而不是抛异常。std::spanC20的非拥有视图代理在传递数组参数时不用拷贝避免了不必要的内存分配。这些特性组合起来能让C代理模式的代码更现代、更安全。比如用std::jthread做一个定期刷新缓存的前台代理可以用RAII管理线程生命周期代理析构时自动停止线程并join不会遗留后台线程。4. 实操过程与核心实现搭一个能跑的代理框架4.1 一个集缓存、日志、权限于一体的组合代理纸上谈兵太干下面我完整实现一个组合代理。这个代理同时做三件事权限校验、结果缓存、日志审计。很多开源框架里的多级代理本质就是这个思路的排列组合。假设业务接口是查询用户信息#include iostream #include memory #include string #include unordered_map #include mutex #include chrono class UserService { public: virtual ~UserService() default; virtual std::string getUserName(int userId) 0; }; class RealUserService : public UserService { public: std::string getUserName(int userId) override { // 模拟数据库查询耗时操作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); return 用户_ std::to_string(userId); } };然后是组合代理类。权限、缓存、日志分别用独立的方法实现主逻辑清晰class CachedLoggingProxy : public UserService { public: enum class AccessLevel { Guest, User, Admin }; CachedLoggingProxy(std::shared_ptrRealUserService real, AccessLevel level) : real_(std::move(real)), accessLevel_(level) {} std::string getUserName(int userId) override { // 1. 权限校验 if (accessLevel_ AccessLevel::Guest) { throw std::runtime_error(无权限访问用户信息); } // 2. 日志调用前记录 auto start std::chrono::high_resolution_clock::now(); std::cout [日志] 查询用户 userId 开始 std::endl; // 3. 缓存检查 { std::lock_guardstd::mutex lock(cacheMutex_); auto it cache_.find(userId); if (it ! cache_.end()) { auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout [日志] 命中缓存, 耗时: duration.count() us std::endl; return it-second; } } // 4. 未命中缓存调用真实服务这里也加了锁保护 std::string result real_-getUserName(userId); // 5. 写入缓存 { std::lock_guardstd::mutex lock(cacheMutex_); cache_[userId] result; } // 6. 日志调用后记录 auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout [日志] 查询完成, 耗时: duration.count() us, 结果: result std::endl; return result; } private: std::shared_ptrRealUserService real_; AccessLevel accessLevel_; std::unordered_mapint, std::string cache_; std::mutex cacheMutex_; };这个类最核心的改进是缓存检查与真实调用之间不持锁。如果让整个函数体在锁范围内执行那当缓存未命中时所有线程都会被阻塞在真实查询上并发性能极差。这里采用先查缓存短锁→未命中则调真实服务无锁→再写回缓存短锁的策略相比全程加锁并发度提升很明显。不过要注意这里存在缓存击穿风险多个线程同时未命中缓存会同时调用真实服务。优化方案是锁升级或双重检查锁但双重检查锁在C里要配合原子变量使用代码复杂度会上一个台阶。对于缓存一致性要求不是极其严格的场景上面这种允许少量重复查询的写法反而更简单实用。4.2 使用组合代理构造与接入调用方不需要知道背后有代理只跟UserService接口打交道int main() { auto realService std::make_sharedRealUserService(); auto proxy std::make_sharedCachedLoggingProxy( realService, CachedLoggingProxy::AccessLevel::User ); // 第一次调用未命中缓存 std::cout proxy-getUserName(1) std::endl; // 第二次调用命中缓存等待时间显著缩短 std::cout proxy-getUserName(1) std::endl; // 用一个Guest权限的代理测试权限拦截 auto guestProxy std::make_sharedCachedLoggingProxy( realService, CachedLoggingProxy::AccessLevel::Guest ); try { guestProxy-getUserName(2); } catch (const std::exception e) { std::cout 捕获异常: e.what() std::endl; } return 0; }运行结果的第一轮输出大概是这样[日志] 查询用户 1 开始 正在模拟数据库查询... [日志] 查询完成, 耗时: 100234us, 结果: 用户_1 用户_1第二次调用同一用户时直接命中缓存耗时从10万微秒级别下降到个位数微秒级别缓存效果一目了然。我用VSCode配置的C/C环境实测过这段代码编译时需要开启C17标准因为用到了std::shared_ptr、std::mutex和std::chrono。如果是在Dev C这种老旧的IDE里跑记得把编译器标准调到C17否则某些头文件会编译报错。接入代理到现有项目的关键点在于尽量以接口类型传递对象。也就是说如果你原来的函数签名是void handle(UserService svc)那传入代理对象也可以正常工作因为代理类也继承了UserService。这样现有调用代码几乎不需要改动只要在对象创建处替换即可。4.3 性能考量代理不是免费的午餐代理模式带来的间接层是有代价的。C开发者必须对性能开销有清醒认识虚函数调用开销如果代理和真实对象都通过虚函数调用每次调用至少多一次间接跳转还可能阻止编译器内联。在低频调用场景下无感知但在每秒百万次的高频循环里这个开销不可忽略。锁开销多线程代理加的锁就算没有竞争也有大约几十纳秒级的开销。如果锁竞争严重线程阻塞和唤醒的开销会更大。缓存的内存开销代理里如果维护一个缓存map需要额外内存还要考虑缓存淘汰策略否则内存会无限增长。对象构造次数代理本身是额外对象构造和析构也需要时间。如果代理被频繁创建销毁那开销会叠加。性能优化建议能用模板代理就不用虚函数代理模板代理在编译器可以内联调用开销几乎为零。代价是灵活性降低。缓存只做在读多写少的场景如果数据频繁更新缓存命中率低还得维护缓存一致性不如不做。锁的粒度要粗中带细组合操作加一把大锁简单操作加小锁。读多写少用shared_mutex写多读少直接用mutex甚至不用锁。用C17的std::string_view传参代理转发时避免不必要的字符串拷贝这在日志代理里收益尤其明显。另一个容易被忽略的点是编译期优化。如果你的代理类是模板而且真实对象的实现也在头文件里编译器有机会把整个代理调用链内联成一个函数调用性能损失几乎为零。这也是为什么有些性能敏感的开源库里代理模式更喜欢用模板而不是虚函数。5. 常见问题与排查技巧实录5.1 代理为什么没有生效这是我在公司带新人时被问得最多的问题。一段代理代码看起来没问题但运行时发现真实对象的方法还是被直接调用了或者代理逻辑根本没执行。排查思路通常按这个顺序检查是否真的拿到了代理对象。有时候业务代码里某个地方缓存了真实对象的指针后续都通过这个指针调用代理就被绕过了。解决办法是全局统一用工厂函数返回代理对象禁止直接new真实对象。检查是否符合多态条件。代理继承抽象接口真实对象也继承抽象接口但如果你想通过基类指针调用代理基类必须有虚析构函数。如果基类没有虚析构函数通过基类指针delete代理对象就是未定义行为表现可能是程序崩溃、内存泄漏或者看起来正常但实际上行为诡异。检查对象切片。如果你把代理对象按值传递给一个接受基类参数的函数会发生对象切片。代理的派生类部分被切掉只剩基类部分多态彻底失效代码压根不会调用代理的override函数。解决办法是传指针或引用。检查拷贝行为。如果你的代理类被刻意设计成不可拷贝但代码里不小心按值拷贝了它编译器可能悄悄动用了移动或拷贝构造函数导致代理的内部状态比如缓存、真实对象指针丢失。解决办法是显式禁用拷贝或正确实现拷贝语义。5.2 性能不升反降代理到底该不该用代理模式被滥用的情况相当常见。我见过一个团队给所有服务类都套上代理结果就是一次业务请求要经过六层代理转发每一层都在做权限检查和日志记录调用链深得可怕。这种代理使用的核心问题是代理本身不产生价值却徒增了复杂度和调用开销。判断要不要用代理我的标准很简单真实对象能否被你直接修改如果能而且只是加一两行日志那直接在真实对象里加日志就行了何必多一层代理。调用方能不能做到不关心真实对象如果调用方必须知道具体类型那代理的价值就很低。是否有多处调用方需要统一增强只有一处调用的话直接在调用方做逻辑比代理更省事。你的代理是否会成为性能瓶颈如果代理里有锁、有网络请求、有磁盘IO那它就是瓶颈需要考虑异步化或批处理。还有一个很常见的过度设计迹象代理里嵌套代理嵌套两层以上还说得过去嵌套四层五层就是灾难。每层代理都有各自的异常处理、锁和日志调试的时候根本不知道异常从哪层冒出来的代码维护成本直线上升。我建议最多两层比如一层负责权限校验一层负责缓存再嵌套业务逻辑就已经很复杂了。5.3 常见问题速查表我把实际工程中踩过的坑整理成一张速查表方便你排查。症状可能原因解决办法代理不生效直接调用了真实对象调用方持有的是真实对象指针/引用统一通过工厂函数获取代理对象程序崩溃或内存泄漏基类缺少虚析构函数在抽象接口里声明virtual ~Subject() default;行为错误但无崩溃对象切片按值传递代理对象改用指针或引用传递双重释放double free代理和外部都delete了同一真实对象所有权统一由shared_ptr管理多线程下缓存被重复填充缓存检查与写入之间存在竞态加短锁或用双重检查锁原子变量锁竞争导致性能下降锁粒度太粗整个方法全程持锁缩小锁范围读多写少用读写锁死锁两个代理互相调用各自持锁等待对方统一锁顺序或使用std::recursive_mutex代理生命周期比真实对象长悬垂指针崩溃随机出现用weak_ptr观察真实对象过期则重建嵌套代理异常信息丢失内层代理异常被外层捕获后吞掉用std::exception_ptr透传不要空catch我专门强调一下proxy生命周期比真实对象长这个坑。如果你用shared_ptr管理真实对象但代理是裸持有真实对象的指针那当shared_ptr计数归零、真实对象被销毁之后代理再调用真实对象的方法就是经典悬垂指针问题。解决方法是代理内部持有weak_ptr每次调用前lock()检查真实对象是否还活着class SafeProxy : public Subject { public: explicit SafeProxy(std::weak_ptrRealSubject real) : real_(std::move(real)) {} void operation() override { if (auto real real_.lock()) { real-operation(); } else { throw std::runtime_error(真实对象已被销毁); } } private: std::weak_ptrRealSubject real_; };这种写法的好处是代理的生命周期可以和真实对象解耦。即便真实对象先被销毁代理也不会崩溃而是给出明确错误提示。这在插件化架构或动态卸载模块的场景下非常有用。6. 几个我从实战里沉淀下来的小技巧代理模式学到一定程度拼的就不再是会不会写而是写得够不够地道。分享几个我积累的经验。第一优先组合而不是继承。很多人一写代理就继承抽象接口但如果代理逻辑很复杂比如又缓存又校验又日志可以考虑把每一块逻辑拆成单独的小代理然后用组合的方式嵌套起来。这样每个代理职责单一组合关系清清楚楚测试也好写。第二给代理类加上static create工厂函数。C里没有Java那么方便的依赖注入容器但工厂函数可以让你集中控制代理的创建逻辑。比如class UserServiceProxy : public UserService { public: static std::shared_ptrUserService create(AccessLevel level) { auto real std::make_sharedRealUserService(); return std::make_sharedCachedLoggingProxy(real, level); } };调用方只需要UserServiceProxy::create(AccessLevel::Admin)不用关心背后创建了哪些对象。以后要增加新的代理层级只改工厂函数内部即可。第三注意代理中转发参数的完美转发。如果真实对象的方法有多个重载或者参数类型不同代理转发时用std::forward保留参数的左值/右值属性避免不必要的拷贝。这些细节在性能敏感代码里差别很大。第四测试代理的边界条件。代理逻辑越丰富出错概率越高。我现在写代理最少配三个测试用例正常路径、异常路径、并发路径。并发测试不一定要求零bug但至少能暴露死锁和悬垂问题。特别是权限代理一定要测试无权限调用被拒绝这条路径别只测有权限时的正常流程。代理模式还有个有趣的扩展方向作为实现AOP的雏形。你可以在代理的方法前后自动插入耗时统计、参数日志、异常记录这跟Spring AOP的思想是一致的只不过C需要手动搭建这些能力。如果你公司里有自研的框架代码大概率能看到代理模式以各种变体存在于中间层组件里。最后再分享一个调试技巧。如果你在VSCode里调试代理代码设断点时记得同时添加条件断点比如只在userId 1000时触发。代理转发链路人一多每次调用都断一次会非常烦。另外在代理层打印日志时用__PRETTY_FUNCTION__或__func__能快速定位当前调用栈所处的函数名减少翻代码找函数的时间。这些细节在排查复杂线上问题时会帮大忙。

相关新闻