恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C++策略模式不止一种写法:从虚函数到std::variant的六种实现与选型指南

  • 首页
  • 资讯中心
  • /
  • C++策略模式不止一种写法:从虚函数到std::variant的六种实现与选型指南

相关资讯

写给初中级Java开发者:如何系统提升代码质量与工程能力 2026/9/9 9:03:38
Java工程师必会的五种设计模式,第二种写代码时天天用 2026/9/9 9:03:38
Cadence SiP Layout:从PCB思维到多物理场协同设计 2026/9/9 9:03:38

最新资讯

HTOOL-SL6H便携信号源评测:从按键到SCPI的射频测试实战指南
Python作品集实战:5个项目帮你搞定面试官
Magnitude:本地大模型服务的协议抽象层与Agent编排枢纽
免费音频转文字工具实测:录音转文本、视频转字幕与格式处理全攻略
AI Agent记忆系统三层架构:短期、长期与工作记忆实战解析
Magnitude:从向量模长到星等震级,理解“量级”如何重塑技术决策

今日推荐

基于MongoDB的图书管理系统:数据建模与Spring Boot+Vue实战
Claude Code安装配置全攻略:从零开始用上终端AI编程助手
tmux 会话管理与终端复用:AI 编程工作流的调度中枢实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

C++策略模式不止一种写法:从虚函数到std::variant的六种实现与选型指南

发布时间:2026/9/9 9:03:38
C++策略模式不止一种写法:从虚函数到std::variant的六种实现与选型指南 1. 策略模式在C里的标准答案并不标准很多人第一次学策略模式都是从那本经典的GoF书开始的。书上告诉你定义算法族分别封装起来让它们可以互相替换。于是你写出了三个类一个抽象策略基类、几个具体策略子类、一个持有策略对象的Context。这套写法在Java里顺理成章在C里也能跑通但它只是整个策略模式变体家族中最朴素的一个分支。我见过最多的情况是项目里一开始照搬了这个套路用shared_ptrIStrategy当成员变量运行期通过setStrategy切换算法。结果代码能跑但总有几个地方不对劲——值类型被无缘无故变成了引用计数对象每个策略都要在堆上分配策略之间共享状态时还要额外管理生命周期。更麻烦的是这套写法把策略接口的虚函数当成了唯一解导致后面想在编译期固定策略、或者想用std::function做轻量注入时代码结构已经被锁死了。为什么C里的策略模式会演化出这么多变体根子上是因为C同时具备运行期多态和编译期多态两条路再加上值语义、模板、类型擦除这些工具同一个策略概念可以被表达成好几种完全不同的形态。这篇文章我不会只给你一个标准实现而是把我在实际项目里真正用过的几种变体一个一个拆开每种是什么、怎么用、牺牲了什么、换来了什么最后再说怎么选。2. 运行时策略变体虚函数、函数指针与std::function2.1 传统虚函数策略的完整实现先看最经典的一段代码假装我们在写一个压缩模块希望调用方在运行时决定用gzip还是lz4压缩数据class ICompressor { public: virtual ~ICompressor() default; virtual std::vectorchar compress(const std::vectorchar data) 0; }; class GzipCompressor : public ICompressor { public: std::vectorchar compress(const std::vectorchar data) override { // 调用gzip库这里省略实现 return {}; } }; class Lz4Compressor : public ICompressor { public: std::vectorchar compress(const std::vectorchar data) override { // 调用lz4库这里省略实现 return {}; } }; class CompressionContext { public: explicit CompressionContext(std::shared_ptrICompressor compressor) : compressor_(std::move(compressor)) {} void setCompressor(std::shared_ptrICompressor compressor) { compressor_ std::move(compressor); } std::vectorchar compress(const std::vectorchar data) { return compressor_-compress(data); } private: std::shared_ptrICompressor compressor_; };这段代码的优点大家都很熟符合开闭原则新增策略只需要继承ICompressor不用改Context运行时通过setCompressor切换策略。但它有四个容易被忽略的代价。第一是虚函数调用开销。每次调用compress都要走一次虚表间接寻址现代CPU分支预测对同一路径持续命中的情况还算友好但如果你在热循环里频繁切换不同策略对象虚表缓存失效的影响会被放大。第二是堆分配。shared_ptrICompressor意味着策略对象几乎必然活在堆上。每次创建新策略都要走分配器如果Context在循环里被频繁构造这堆分配可不是免费的。第三是生命周期管理。Context持有的是shared_ptr那策略对象归谁释放如果策略内部又反向持有Context一个不经意的循环引用就能制造内存泄漏。很多人最后不得不改成裸指针加外部所有权风险又转移给了调用方。第四是接口膨胀。只要有一个新策略需要不同的初始化参数你就得在抽象基类里加虚函数或者提供一个别扭的初始化接口。比如某个策略要从配置文件加载参数另一个策略直接无参构造类层级设计会越来越僵硬。这四点不是劝退虚函数方案而是说明这个变体的适用场景是有明确边界的。当策略数量可能持续增长、策略本身带有状态且生命周期较长、运行期切换是硬需求时虚函数方案依然是最合适的选择。它的可扩展性是靠继承这种开放方式保证的这是其他变体很难替代的。2.2 函数指针与std::function放弃继承的轻量替换如果策略只是一个算法入口不需要保存额外状态完全可以把策略降级成一个可调用对象。C语言时代我们会写函数指针class CompressionContext { public: using CompressFn std::vectorchar(*)(const std::vectorchar); explicit CompressionContext(CompressFn fn) : fn_(fn) {} void setCompressFn(CompressFn fn) { fn_ fn; } std::vectorchar compress(const std::vectorchar data) { return fn_(data); } private: CompressFn fn_; }; std::vectorchar gzip_compress(const std::vectorchar data) { return {}; } std::vectorchar lz4_compress(const std::vectorchar data) { return {}; }函数指针方案砍掉了继承、虚表和堆分配策略Context可以直接做成轻量值对象。它的天花板也很明显函数指针没法携带状态而真实项目里大量策略本身就是有状态的。比如一个带字典优化的压缩算法内部要维护一张动态构建的哈希表——你用函数指针怎么塞这个状态只能靠全局变量那就退回到了最糟糕的共享状态时代。更好的替代是std::function它本质一份类型擦除封装内部可以存储任意可拷贝可调用对象包括lambda、函数对象、绑定表达式。上一段函数指针版本可以直接改成class CompressionContext { public: explicit CompressionContext(std::functionstd::vectorchar(const std::vectorchar) fn) : fn_(std::move(fn)) {} void setCompressFn(std::functionstd::vectorchar(const std::vectorchar) fn) { fn_ std::move(fn); } std::vectorchar compress(const std::vectorchar data) { return fn_(data); } private: std::functionstd::vectorchar(const std::vectorchar) fn_; }; CompressionContext make_gzip_context() { return CompressionContext([](const std::vectorchar data) { // gzip压缩逻辑 return std::vectorchar{}; }); }用std::function之后策略的定义不再强制要求有个类叫GzipCompressor你可以在模块内部随意写一个lambda捕获自己的状态然后注入到Context里。这样策略的粒度可以非常细——甚至可以细到两个lambda被同一个业务逻辑调用具备不同的捕获状态。代价也是明确的std::function的调用不一定比虚函数快。它内部可能进行一次小对象优化但如果捕获的状态超过它的inline容量还是要走堆分配。在热路径上这份开销不容忽视。而且类型擦除意味着调试时看到的类型信息不如继承体系清晰你没法在调试器里轻易展开一条完整的策略类层级。那么什么时候该选std::function我的经验是策略功能边界清晰、数量少、切换点散落在代码各处且调用方不太关心策略的具体类型。比如消息队列里不同事件处理器都可以注册成一个std::function这种设计很自然。反过来如果你真的定义了一个完整的多态策略族内部还有紧密协作的多个方法就别硬把它们塞进一个std::function那会把接口扭曲掉。2.3 运行时变体该怎么选把这三种运行时方案放在一张表里对比看起来会更清楚维度虚函数继承函数指针std::function是否支持状态支持不支持支持类型信息完整性完整弱被擦除调用开销虚表间接调用直接调用类型擦除间接调用代码侵入性需要继承体系极低低扩展性开放新增子类只能换函数灵活lambda等调试友好度好一般一般所以我的结论是涉及完整策略体系、有状态、需要统一管理生命周期时优先虚函数策略是纯函数式、无状态且调用非常频繁时函数指针是最便宜的方案而处于中间地带、希望享受lambda便利性又不介意少一点类型信息时std::function通常是平衡得最好的选择。3. 编译期策略变体模板策略与CRTP3.1 模板策略把策略写进类型前面说的三种方案都是在运行期决定调用谁的算法但很多场景下策略在编译期就已经定死了。比如一个嵌入式设备固件编译时就知道用哪个压缩算法根本不需要运行期切换。这时候再用虚函数和std::function等于白付运行期代价。把策略作为模板参数传入是最直接的编译期策略变体template typename CompressionPolicy class CompressionContext { public: std::vectorchar compress(const std::vectorchar data) { return CompressionPolicy::compress(data); } }; struct GzipPolicy { static std::vectorchar compress(const std::vectorchar data) { // gzip实现 return {}; } }; struct Lz4Policy { static std::vectorchar compress(const std::vectorchar data) { // lz4实现 return {}; } }; using GzipContext CompressionContextGzipPolicy; using Lz4Context CompressionContextLz4Policy;注意这里的策略不再需要继承任何基类它只需要提供满足约定的compress方法。这种约束在C20里可以用concept显式声明C17及之前则靠编译错误倒逼接口对齐。模板策略带来的核心优势是零虚函数开销、零堆分配编译器通常能拿到完整上下文做内联优化性能可以达到和手写直调完全一致的水平。代价是策略在编译期固定运行期没法换。如果同一个Context对象需要根据用户输入切换策略模板方案就不适合。另一个问题是Container本身也会变成模板所有持有CompressionContext的地方都要模板化这在大型项目里会像涟漪一样传播出去。3.2 CRTP实现静态多态策略模板策略的缺点是策略必须是无状态的静态方法或者至少得把状态抛出去让Context持有。但有时候策略本身的多个方法之间需要共享状态而且它们要访问Context的受保护信息。这时候用CRTP奇异递归模板模式更顺手。CRTP的基本形态是基类模板接受派生类作为模板参数基类通过向下转型调用派生类实现template typename Derived struct CompressorStrategyBase { std::vectorchar compress(const std::vectorchar data) { return static_castDerived*(this)-compress_impl(data); } std::vectorchar decompress(const std::vectorchar data) { return static_castDerived*(this)-decompress_impl(data); } }; struct GzipStrategy : CompressorStrategyBaseGzipStrategy { std::vectorchar compress_impl(const std::vectorchar data) { // gzip实现可以使用成员变量 return {}; } std::vectorchar decompress_impl(const std::vectorchar data) { return {}; } private: std::vectoruint8_t dictionary_; };使用时Context通过模板参数接收策略类型但策略对象本身可以作为成员直接持有template typename Strategy class CompressionContext { public: std::vectorchar compress(const std::vectorchar data) { return strategy_.compress(data); } private: Strategy strategy_; }; CompressionContextGzipStrategy ctx;这里的compress虽然定义在基类里但实际调用会被编译期解析到GzipStrategy::compress_impl没有虚表跳转。CRTP最妙的地方是它让策略基类可以提供一组默认实现被某个派生类选择性覆盖这一点和虚函数有点像但绑定完全发生在编译期。不过CRTP不是银弹。它把策略模式和模板高度耦合代码可读性比虚函数差一截。报错信息里密密麻麻的模板实例化记录新人看一眼就想跑。而且CRTP需要你非常清楚对象生命周期因为你拿到的是Derived*如果基类析构函数不是虚的通过基类指针删除对象就会触发未定义行为。好在典型用法里我们通常不通过基类指针删除策略而是直接持有具体类型这个问题可以避开。3.3 编译期策略的代价与应对编译期策略听起来很美好但有一类成本常被人忽略编译时间。把一个策略体系模板化后每实例化一种新的策略组合编译器都要重新生成一份完整代码。如果你的Context被十几个cpp文件引用每个文件都实例化一遍相同策略链接器虽然能合并大部分重复符号编译和代码生成阶段的耗时还是会肉眼可见地增长。应对办法有几种。一是把模板内部完全暴露的非模板部分拆到非模板基类中让公共逻辑只编译一次二是用显式模板实例化把常见的策略组合固定在某个cpp中减少重复实例化三是如果策略数量确实很少可以直接用if constexpr在编译期过滤类型而不是让整个Context变成模板。比如下面这种场景——通过一个StrategyTag在编译期决定调用哪个算法enum class StrategyTag { Gzip, Lz4 }; template StrategyTag Tag std::vectorchar dispatch_compress(const std::vectorchar data) { if constexpr (Tag StrategyTag::Gzip) { return gzip_compress(data); } else if constexpr (Tag StrategyTag::Lz4) { return lz4_compress(data); } }这种写法在编译期完成分支消除代码却仍然保留在单个函数模板里调用方只需要传一个枚举值不需要被模板Context传染。它适合策略分支数量少、处理逻辑简单、不需要保存复杂状态的情况。4. 值语义策略变体std::variant与std::visit4.1 用variant表达封闭策略集前面几种变体要么把策略变成可继承的多态对象要么把策略变成模板参数。还有一种非常符合C现代风格的思路直接把所有可能的策略装进一个std::variant让策略变为纯值语义。这种方案的适用前提是策略集封闭且有限比如一个通信协议库压缩算法只支持None、Gzip、Zstd三种未来也不打算开放给用户扩展。这种场景下你根本不需要继承体系或者模板一个 variant 就能完成所有需求struct NoCompression { std::vectorchar compress(const std::vectorchar data) { return data; } std::vectorchar decompress(const std::vectorchar data) { return data; } }; struct GzipCompression { std::vectorchar compress(const std::vectorchar data) { return {}; } std::vectorchar decompress(const std::vectorchar data) { return {}; } }; struct ZstdCompression { std::vectorchar compress(const std::vectorchar data) { return {}; } std::vectorchar decompress(const std::vectorchar data) { return {}; } }; using CompressionStrategy std::variantNoCompression, GzipCompression, ZstdCompression; class CompressionContext { public: explicit CompressionContext(CompressionStrategy strategy) : strategy_(std::move(strategy)) {} std::vectorchar compress(const std::vectorchar data) { return std::visit([](const auto strategy) { return strategy.compress(data); }, strategy_); } private: CompressionStrategy strategy_; };用这种方案的第一个好处是省去了继承每个策略类型不需要共享基类它们只需要在访问者访问时提供一致的调用接口。如果某个策略的接口不同访问者里可以用if constexpr针对不同类型走不同分支。第二个好处是值语义完全落地。CompressionContext拷贝、移动、存储都自然不再需要管理生命周期。没有堆分配没有共享所有权缓存友好度也更高。在性能敏感、策略存储在vector里做批量处理的场景下这种方案的优势很明显。第三个好处是编译期穷尽性。std::visit要求访问者对 variant 的所有备选类型都要有可调用的处理逻辑这在编译期就保证了你不会漏掉某个策略。4.2 访问者如何落地一个常见的坑是直接把lambda写得很长把所有策略逻辑都塞进std::visit的泛型lambda里。这样虽然能编译但代码可读性极差。更好的做法是定义专门的访问者结构体把不同策略的逻辑拆到独立的重载函数中struct CompressionVisitor { std::vectorchar operator()(const NoCompression, const std::vectorchar data) const { return data; } std::vectorchar operator()(const GzipCompression, const std::vectorchar data) const { return gzip_compress(data); } std::vectorchar operator()(const ZstdCompression, const std::vectorchar data) const { return zstd_compress(data); } };配合std::visit使用std::vectorchar result std::visit([](const auto strategy) { return CompressionVisitor{}(strategy, data); }, strategy_);这里我用了两层封装外层泛型lambda负责让std::visit能通过重载决议选到正确的operator()内层CompressionVisitor承担真正的逻辑分发。这种写法在策略方法很多时尤其有用你可以在CompressionVisitor里维护一组私有的辅助函数而不是把所有逻辑都堆在 lambda 里。还要注意std::variant的默认构造行为。默认构造会激活第一个备选类型并且要求第一个备选类型必须是可默认构造的。如果你不希望策略可以被无意义地默认初始化可以用std::monostate占位或者删掉默认构造函数。实际项目中我经常在 variant 里塞一个std::monostate专门用来表达当前策略未配置的状态避免用某个具体策略类型去硬扛。4.3 variant策略的边界条件用std::variant表达策略有一个硬伤策略集合是封闭的。如果你今天写了一个CompressionStrategy明天用户通过插件系统往里面加入一种新压缩算法variant 就无能为力了——你必须在编译期把所有备选类型都写进去。这违背了很多教科书里策略模式是为了支持运行时扩展的核心论调。所以我的判断标准很明确当策略的扩展点是开放的选虚函数或std::function当策略的扩展点封闭但每个策略内部逻辑又相对复杂时选 variant 是一种非常值得考虑的做法它换个角度把策略模式的灵活性从对象多态转移到了编译期穷尽保证上。另外variant 的每个备选类型内部如果有大量成员那么 variant 对象本身会变得很大。因为 variant 要占用所有备选类型中最大的那个字节数。如果某个策略内部有巨大的缓存或配置表其他所有策略都会被拖累内存占用会显著升高。这种时候最好让 variant 内部存的是状态数据的小型对象或者退回去用指针。5. 实际项目中的策略选择方法与避坑经验5.1 先问四个问题我从不在拿到需求后立刻写代码。选哪种策略变体先问自己四个问题第一个问题策略集合是否封闭如果未来会有外部模块新增策略那就别用 variant优先考虑虚函数或std::function。如果策略集合在可预见的未来只有三五种别急着架一堆继承体系variant 甚至模板都能应付。第二个问题运行期切换是必须的吗如果策略在Context构造时就固定了但调用方希望能在运行时通过配置动态选择std::function和虚函数都能满足如果策略在程序启动前就编译期确定模板策略明显更省。千万别为了可能以后要动态切换而提前付出虚函数和堆分配的成本这种假设大多数情况下不会发生。第三个问题策略内部有状态吗有状态且生命周期与Context不一致时虚函数方案配合unique_ptr或shared_ptr比较自然有状态但生命周期完全在Context内部时CRTP和variant都能用无状态时函数指针和模板静态方法是最轻的。第四个问题性能要求有多高如果策略调用位于每秒百万级的循环里虚函数和std::function的开销都会被放大模板和 variant 更有优势如果策略调用本身耗时在毫秒级这些差异微不足道优先可维护性。5.2 场景一运行时配置 vs 编译期装配我做过一个配置驱动的流式计算系统每种数据处理节点都对应不同的算法配置中心下发一个字符串程序要动态创建对应的处理节点。这种情况下我选了std::function加注册表class NodeFactory { public: using NodeFactoryFn std::functionstd::unique_ptrNode(const Config); static NodeFactory instance() { static NodeFactory factory; return factory; } void registerNode(std::string_view type, NodeFactoryFn fn) { factories_[std::string(type)] std::move(fn); } std::unique_ptrNode create(std::string_view type, const Config cfg) const { auto it factories_.find(std::string(type)); if (it factories_.end()) { throw std::runtime_error(unknown node type); } return it-second(cfg); } private: std::unordered_mapstd::string, NodeFactoryFn factories_; };每个节点类型在启动时把自己注册进去配置驱动时只需要查表创建。这里没有继承策略基类策略就是一个返回unique_ptrNode的工厂函数。这种方案让新增节点类型变得非常轻只需要在某个模块的初始化函数里调用registerNode即可。另一个场景是编译期装配——某个固定功能的命令行工具参数只有一个--algo但所有算法已经在代码里写死。这时候我用模板策略--algo只影响启动时选择哪个模板实例int run_with_policy(StrategyTag tag, const std::vectorstd::string args) { switch (tag) { case StrategyTag::Gzip: return run_implGzipPolicy(args); case StrategyTag::Lz4: return run_implLz4Policy(args); } return -1; }这段代码的运行期分支只发生一次之后所有调用都在编译期确定了。对比起来运行时配置场景更强调灵活扩展编译期装配场景更强调执行效率。5.3 场景二策略生命周期与线程安全策略模式在真实项目里最隐蔽的坑是策略对象被多个线程共享。如果策略内部有状态且不支持并发访问那么不管用哪种变体都得面对并发问题。虚函数方案里策略对象通常被shared_ptr共享为了线程安全要么加锁要么把状态设计成线程局部std::function方案里lambda捕获的缓冲同样可能被并发调用。有一个被低估的解决办法是把策略设计成不可变配置 每次调用产生局部状态两种分离结构。策略本身只存不可变配置真正运行时的中间状态放在调用栈里。这样共享策略对象就是安全的。比如压缩算法的配置项压缩级别、字典大小是不可变的而压缩过程中的工作缓冲区是函数内的局部变量。这种设计让策略无论用哪种变体都能轻松应对多线程。如果你发现某个策略确实需要在两次调用之间保留动态可变的内部状态那么使用模板策略时你会收获一个惊喜每个线程可以持有自己的CompressionContextCompressionPolicy因为模板类型的实例化天然是独立的线程之间根本不共享策略对象不需要加锁。这也是编译期变体在并发环境里的一个隐藏优势。5.4 场景三与工厂、依赖注入组合策略模式很少单独出现它最常和工厂模式、依赖注入混在一起。我的经验是工厂负责创建策略依赖注入负责把策略交给使用方策略模式本身只负责定义算法可替换的边界。这三者分工清晰别把工厂逻辑塞进Context。实用建议是让Context的构造函数只接受策略对象不接收策略的构造参数。策略的构建交给工厂避免Context被策略创建细节污染class CompressionContext { public: explicit CompressionContext(CompressionStrategy strategy) : strategy_(std::move(strategy)) {} // ... }; class CompressionFactory { public: static CompressionStrategy create(std::string_view type) { if (type none) return NoCompression{}; if (type gzip) return GzipCompression{}; if (type zstd) return ZstdCompression{}; throw std::invalid_argument(unknown compression type); } };在测试时这种组合方式尤其方便。你可以传一个 mock 策略进 Context不需要 mock 一个庞大的策略基类只需要构造一个行为简单的策略对象。尤其在 variant 方案里mock 就是新增一个备选类型测试看得见摸得着。6. 面试与学习中的策略模式考察点6.1 为什么别急着回答用虚函数C面试里但凡出了讲一下策略模式大多数人的回答都是定义一个抽象接口写几个实现类客户端持有接口指针。这个答案严格来说没错但在C语境里它只是一个非常片面的答案。面试官如果追问一句如果这个策略在编译期就能确定你还会不会这么设计很多人就卡住了。更好的答题思路是先点出策略模式的核心是将算法封装成可独立变化的对象使算法与使用方解耦然后立即说明在C里这个变化可以发生在两个维度——运行期和编译期并分别举一个例子。比如运行期用虚函数或std::function编译期用模板或CRTP。如果你能把 variant 方案也讲出来并解释它适合封闭策略集、值语义、无虚调用面试官大概率会眼前一亮。面试官还经常拿策略模式和状态模式做对比。这两个模式的结构极其相似都是把行为委托给另一个对象但意图完全不同策略模式是让外部调用方主动选择算法算法的执行路径稳定状态模式是让对象根据内部状态自动切换行为行为空间是理论上的全集外部调用方通常不感知状态切换过程。在C语境里策略模式普遍关注哪个算法状态模式更关注当前处于什么状态。另一个高频对比是策略模式和命令模式。策略模式解决的是算法选择问题命令模式解决的是动作封装与延迟执行问题。策略对象通常不保存请求的上下文命令对象则会把请求接收者绑定在自身内部。很多代码把这两者混淆写出来的类既承担算法选择又承担请求参数保存职责边界非常模糊。6.2 容易混淆的模式边界我见过不少项目里出现策略模式的变体用得过度的情况。一个很常见的错误是只有一个算法却也套上了策略接口。这种过度设计会带来不必要的间接层让代码难以阅读。策略模式的本质价值在于存在多个可替换算法且替换行为发生在运行时或通过参数化发生如果你只有一个算法直接写普通函数函数调用即可。第二个容易踩的坑是在模板策略里硬塞虚函数。有些人的Context写成模板但策略接口仍然继承一个虚基类等于同时负担了编译期和运行期的复杂度既没有拿到编译期优化的好处又引入了虚调用的成本。选用策略变体时要有意识地二选一要么走编译期要么走运行期不要无意义地混合。第三个坑是使用std::function时不控制类型约束。std::function几乎可以接受任何可调用对象这既是优点也是隐患。两个不同含义的策略函数可能签名恰好一样结果被意外替换掉。缓解办法是定义强类型别名或为每个策略单独定义一个函数对象类型甚至考虑用std::function外面包一层语义明确的struct避免裸的std::function满天飞。面试和实际项目有一个共同点没有人需要你背出所有变体的代码但你需要展示出知道不同变体各有取舍的工程判断力。我会在面试时直接用例子说明——虚函数方案适合开放扩展模板方案适合封闭且性能敏感variant方案适合封闭但强值语义std::function适合轻量回调注入。把这些边界条件讲清楚比单纯背诵GoF原文有说服力得多。最后再分享一个小技巧不管选哪种变体写测试的时候都别直接依赖真正的算法实现而是先用一个方法体为空的桩策略把Context跑通。这样你验证的是策略调度的正确性而不是策略本身的正确性。很多C项目把策略实现和策略调度混在一起测试出问题时根本分不清是Context分发出错还是策略算法算错了。把这两层测试拆开调试效率会高得多。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号