news 2026/9/9 9:03:29

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++策略模式不止一种写法:从虚函数到std::variant的六种实现与选型指南

1. 策略模式在C++里的"标准答案"并不标准

很多人第一次学策略模式,都是从那本经典的GoF书开始的。书上告诉你:定义算法族,分别封装起来,让它们可以互相替换。于是你写出了三个类:一个抽象策略基类、几个具体策略子类、一个持有策略对象的Context。这套写法在Java里顺理成章,在C++里也能跑通,但它只是整个策略模式变体家族中最朴素的一个分支。

我见过最多的情况是:项目里一开始照搬了这个套路,用shared_ptr<IStrategy>当成员变量,运行期通过setStrategy切换算法。结果代码能跑,但总有几个地方不对劲——值类型被无缘无故变成了引用计数对象,每个策略都要在堆上分配,策略之间共享状态时还要额外管理生命周期。更麻烦的是,这套写法把策略接口的虚函数当成了唯一解,导致后面想在编译期固定策略、或者想用std::function做轻量注入时,代码结构已经被锁死了。

为什么C++里的策略模式会演化出这么多变体?根子上是因为C++同时具备运行期多态和编译期多态两条路,再加上值语义、模板、类型擦除这些工具,同一个"策略"概念可以被表达成好几种完全不同的形态。这篇文章我不会只给你一个"标准实现",而是把我在实际项目里真正用过的几种变体一个一个拆开:每种是什么、怎么用、牺牲了什么、换来了什么,最后再说怎么选。

2. 运行时策略变体:虚函数、函数指针与std::function

2.1 传统虚函数策略的完整实现

先看最经典的一段代码,假装我们在写一个压缩模块,希望调用方在运行时决定用gzip还是lz4压缩数据:

class ICompressor { public: virtual ~ICompressor() = default; virtual std::vector<char> compress(const std::vector<char>& data) = 0; }; class GzipCompressor : public ICompressor { public: std::vector<char> compress(const std::vector<char>& data) override { // 调用gzip库,这里省略实现 return {}; } }; class Lz4Compressor : public ICompressor { public: std::vector<char> compress(const std::vector<char>& data) override { // 调用lz4库,这里省略实现 return {}; } }; class CompressionContext { public: explicit CompressionContext(std::shared_ptr<ICompressor> compressor) : compressor_(std::move(compressor)) {} void setCompressor(std::shared_ptr<ICompressor> compressor) { compressor_ = std::move(compressor); } std::vector<char> compress(const std::vector<char>& data) { return compressor_->compress(data); } private: std::shared_ptr<ICompressor> compressor_; };

这段代码的优点大家都很熟:符合开闭原则,新增策略只需要继承ICompressor,不用改Context;运行时通过setCompressor切换策略。但它有四个容易被忽略的代价。

第一是虚函数调用开销。每次调用compress都要走一次虚表间接寻址,现代CPU分支预测对"同一路径持续命中"的情况还算友好,但如果你在热循环里频繁切换不同策略对象,虚表缓存失效的影响会被放大。

第二是堆分配。shared_ptr<ICompressor>意味着策略对象几乎必然活在堆上。每次创建新策略都要走分配器,如果Context在循环里被频繁构造,这堆分配可不是免费的。

第三是生命周期管理。Context持有的是shared_ptr,那策略对象归谁释放?如果策略内部又反向持有Context,一个不经意的循环引用就能制造内存泄漏。很多人最后不得不改成裸指针加外部所有权,风险又转移给了调用方。

第四是接口膨胀。只要有一个新策略需要不同的初始化参数,你就得在抽象基类里加虚函数,或者提供一个别扭的初始化接口。比如某个策略要从配置文件加载参数,另一个策略直接无参构造,类层级设计会越来越僵硬。

这四点不是劝退虚函数方案,而是说明:这个变体的适用场景是有明确边界的。当策略数量可能持续增长、策略本身带有状态且生命周期较长、运行期切换是硬需求时,虚函数方案依然是最合适的选择。它的可扩展性是靠"继承"这种开放方式保证的,这是其他变体很难替代的。

2.2 函数指针与std::function:放弃继承的轻量替换

如果策略只是一个算法入口,不需要保存额外状态,完全可以把"策略"降级成一个可调用对象。C语言时代我们会写函数指针:

class CompressionContext { public: using CompressFn = std::vector<char>(*)(const std::vector<char>&); explicit CompressionContext(CompressFn fn) : fn_(fn) {} void setCompressFn(CompressFn fn) { fn_ = fn; } std::vector<char> compress(const std::vector<char>& data) { return fn_(data); } private: CompressFn fn_; }; std::vector<char> gzip_compress(const std::vector<char>& data) { return {}; } std::vector<char> lz4_compress(const std::vector<char>& data) { return {}; }

函数指针方案砍掉了继承、虚表和堆分配,策略Context可以直接做成轻量值对象。它的天花板也很明显:函数指针没法携带状态,而真实项目里大量策略本身就是有状态的。比如一个带字典优化的压缩算法,内部要维护一张动态构建的哈希表——你用函数指针怎么塞这个状态?只能靠全局变量,那就退回到了最糟糕的共享状态时代。

更好的替代是std::function,它本质一份类型擦除封装,内部可以存储任意可拷贝可调用对象,包括lambda、函数对象、绑定表达式。上一段函数指针版本可以直接改成:

class CompressionContext { public: explicit CompressionContext(std::function<std::vector<char>(const std::vector<char>&)> fn) : fn_(std::move(fn)) {} void setCompressFn(std::function<std::vector<char>(const std::vector<char>&)> fn) { fn_ = std::move(fn); } std::vector<char> compress(const std::vector<char>& data) { return fn_(data); } private: std::function<std::vector<char>(const std::vector<char>&)> fn_; }; CompressionContext make_gzip_context() { return CompressionContext([](const std::vector<char>& data) { // gzip压缩逻辑 return std::vector<char>{}; }); }

std::function之后,策略的定义不再强制要求"有个类叫GzipCompressor",你可以在模块内部随意写一个lambda,捕获自己的状态,然后注入到Context里。这样策略的粒度可以非常细——甚至可以细到两个lambda被同一个业务逻辑调用,具备不同的捕获状态。

代价也是明确的:std::function的调用不一定比虚函数快。它内部可能进行一次小对象优化,但如果捕获的状态超过它的inline容量,还是要走堆分配。在热路径上,这份开销不容忽视。而且类型擦除意味着调试时看到的类型信息不如继承体系清晰,你没法在调试器里轻易展开一条完整的策略类层级。

那么什么时候该选std::function?我的经验是:策略功能边界清晰、数量少、切换点散落在代码各处,且调用方不太关心策略的具体类型。比如消息队列里不同事件处理器都可以注册成一个std::function,这种设计很自然。反过来,如果你真的定义了一个完整的多态策略族,内部还有紧密协作的多个方法,就别硬把它们塞进一个std::function,那会把接口扭曲掉。

2.3 运行时变体该怎么选

把这三种运行时方案放在一张表里对比,看起来会更清楚:

维度虚函数+继承函数指针std::function
是否支持状态支持不支持支持
类型信息完整性完整被擦除
调用开销虚表间接调用直接调用类型擦除间接调用
代码侵入性需要继承体系极低
扩展性开放(新增子类)只能换函数灵活(lambda等)
调试友好度一般一般

所以我的结论是:涉及完整策略体系、有状态、需要统一管理生命周期时,优先虚函数;策略是纯函数式、无状态且调用非常频繁时,函数指针是最便宜的方案;而处于中间地带、希望享受lambda便利性又不介意少一点类型信息时,std::function通常是平衡得最好的选择。

3. 编译期策略变体:模板策略与CRTP

3.1 模板策略,把策略写进类型

前面说的三种方案都是在运行期决定"调用谁的算法",但很多场景下,策略在编译期就已经定死了。比如一个嵌入式设备,固件编译时就知道用哪个压缩算法,根本不需要运行期切换。这时候再用虚函数和std::function,等于白付运行期代价。

把策略作为模板参数传入,是最直接的编译期策略变体:

template <typename CompressionPolicy> class CompressionContext { public: std::vector<char> compress(const std::vector<char>& data) { return CompressionPolicy::compress(data); } }; struct GzipPolicy { static std::vector<char> compress(const std::vector<char>& data) { // gzip实现 return {}; } }; struct Lz4Policy { static std::vector<char> compress(const std::vector<char>& data) { // lz4实现 return {}; } }; using GzipContext = CompressionContext<GzipPolicy>; using Lz4Context = CompressionContext<Lz4Policy>;

注意这里的策略不再需要继承任何基类,它只需要提供满足约定的compress方法。这种约束在C++20里可以用concept显式声明,C++17及之前则靠编译错误倒逼接口对齐。模板策略带来的核心优势是零虚函数开销、零堆分配,编译器通常能拿到完整上下文做内联优化,性能可以达到和手写直调完全一致的水平。

代价是策略在编译期固定,运行期没法换。如果同一个Context对象需要根据用户输入切换策略,模板方案就不适合。另一个问题是Container本身也会变成模板,所有持有CompressionContext的地方都要模板化,这在大型项目里会像涟漪一样传播出去。

3.2 CRTP实现静态多态策略

模板策略的缺点是策略必须是"无状态"的静态方法,或者至少得把状态抛出去让Context持有。但有时候,策略本身的多个方法之间需要共享状态,而且它们要访问Context的受保护信息。这时候用CRTP(奇异递归模板模式)更顺手。

CRTP的基本形态是基类模板接受派生类作为模板参数,基类通过向下转型调用派生类实现:

template <typename Derived> struct CompressorStrategyBase { std::vector<char> compress(const std::vector<char>& data) { return static_cast<Derived*>(this)->compress_impl(data); } std::vector<char> decompress(const std::vector<char>& data) { return static_cast<Derived*>(this)->decompress_impl(data); } }; struct GzipStrategy : CompressorStrategyBase<GzipStrategy> { std::vector<char> compress_impl(const std::vector<char>& data) { // gzip实现,可以使用成员变量 return {}; } std::vector<char> decompress_impl(const std::vector<char>& data) { return {}; } private: std::vector<uint8_t> dictionary_; };

使用时,Context通过模板参数接收策略类型,但策略对象本身可以作为成员直接持有:

template <typename Strategy> class CompressionContext { public: std::vector<char> compress(const std::vector<char>& data) { return strategy_.compress(data); } private: Strategy strategy_; }; CompressionContext<GzipStrategy> 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::vector<char> dispatch_compress(const std::vector<char>& 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::visit

4.1 用variant表达封闭策略集

前面几种变体要么把策略变成可继承的多态对象,要么把策略变成模板参数。还有一种非常符合C++现代风格的思路:直接把所有可能的策略装进一个std::variant,让策略变为纯值语义。

这种方案的适用前提是策略集封闭且有限,比如一个通信协议库,压缩算法只支持NoneGzipZstd三种,未来也不打算开放给用户扩展。这种场景下,你根本不需要继承体系或者模板,一个 variant 就能完成所有需求:

struct NoCompression { std::vector<char> compress(const std::vector<char>& data) { return data; } std::vector<char> decompress(const std::vector<char>& data) { return data; } }; struct GzipCompression { std::vector<char> compress(const std::vector<char>& data) { return {}; } std::vector<char> decompress(const std::vector<char>& data) { return {}; } }; struct ZstdCompression { std::vector<char> compress(const std::vector<char>& data) { return {}; } std::vector<char> decompress(const std::vector<char>& data) { return {}; } }; using CompressionStrategy = std::variant<NoCompression, GzipCompression, ZstdCompression>; class CompressionContext { public: explicit CompressionContext(CompressionStrategy strategy) : strategy_(std::move(strategy)) {} std::vector<char> compress(const std::vector<char>& 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::vector<char> operator()(const NoCompression&, const std::vector<char>& data) const { return data; } std::vector<char> operator()(const GzipCompression&, const std::vector<char>& data) const { return gzip_compress(data); } std::vector<char> operator()(const ZstdCompression&, const std::vector<char>& data) const { return zstd_compress(data); } };

配合std::visit使用:

std::vector<char> 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_ptrshared_ptr比较自然;有状态但生命周期完全在Context内部时,CRTP和variant都能用;无状态时,函数指针和模板静态方法是最轻的。

第四个问题:性能要求有多高?如果策略调用位于每秒百万级的循环里,虚函数和std::function的开销都会被放大,模板和 variant 更有优势;如果策略调用本身耗时在毫秒级,这些差异微不足道,优先可维护性。

5.2 场景一:运行时配置 vs 编译期装配

我做过一个配置驱动的流式计算系统,每种数据处理节点都对应不同的算法,配置中心下发一个字符串,程序要动态创建对应的处理节点。这种情况下我选了std::function加注册表:

class NodeFactory { public: using NodeFactoryFn = std::function<std::unique_ptr<Node>(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_ptr<Node> 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_map<std::string, NodeFactoryFn> factories_; };

每个节点类型在启动时把自己注册进去,配置驱动时只需要查表创建。这里没有继承策略基类,策略就是一个返回unique_ptr<Node>的工厂函数。这种方案让新增节点类型变得非常轻,只需要在某个模块的初始化函数里调用registerNode即可。

另一个场景是编译期装配——某个固定功能的命令行工具,参数只有一个--algo,但所有算法已经在代码里写死。这时候我用模板策略,--algo只影响启动时选择哪个模板实例:

int run_with_policy(StrategyTag tag, const std::vector<std::string>& args) { switch (tag) { case StrategyTag::Gzip: return run_impl<GzipPolicy>(args); case StrategyTag::Lz4: return run_impl<Lz4Policy>(args); } return -1; }

这段代码的运行期分支只发生一次,之后所有调用都在编译期确定了。对比起来,运行时配置场景更强调灵活扩展,编译期装配场景更强调执行效率。

5.3 场景二:策略生命周期与线程安全

策略模式在真实项目里最隐蔽的坑是策略对象被多个线程共享。如果策略内部有状态且不支持并发访问,那么不管用哪种变体,都得面对并发问题。虚函数方案里,策略对象通常被shared_ptr共享,为了线程安全要么加锁,要么把状态设计成线程局部;std::function方案里,lambda捕获的缓冲同样可能被并发调用。

有一个被低估的解决办法是:把策略设计成"不可变配置 + 每次调用产生局部状态"两种分离结构。策略本身只存不可变配置,真正运行时的中间状态放在调用栈里。这样共享策略对象就是安全的。比如压缩算法的配置项(压缩级别、字典大小)是不可变的,而压缩过程中的工作缓冲区是函数内的局部变量。这种设计让策略无论用哪种变体都能轻松应对多线程。

如果你发现某个策略确实需要在两次调用之间保留动态可变的内部状态,那么使用模板策略时你会收获一个惊喜:每个线程可以持有自己的CompressionContext<CompressionPolicy>,因为模板类型的实例化天然是独立的,线程之间根本不共享策略对象,不需要加锁。这也是编译期变体在并发环境里的一个隐藏优势。

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分发出错,还是策略算法算错了。把这两层测试拆开,调试效率会高得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 9:00:29

ViL整车在环测试:从原理到工程落地的智能驾驶验证指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 9:00:04

基于遗传算法的风电混合储能容量优化配置及MATLAB实现

我做了几年风电功率预测和储能配置的项目&#xff0c;接触过不少用 MATLAB 做容量优化的需求&#xff0c;其中“基于遗传算法的风电混合储能容量优化配置”这个方向问的人最多。原因也简单&#xff1a;风电出力天生波动&#xff0c;光靠单一储能削峰填谷要么贵得离谱&#xff0…

作者头像 李华
网站建设 2026/9/9 8:59:55

三大AI聚合平台协议兼容性横评:OpenAI/Anthropic/Gemini真实接入实测

先说一个我自己的真实感受&#xff1a;今年年初我们团队把内部 AI 能力从“只接一家模型”改成“多模型自由路由”&#xff0c;最大的痛点不是模型效果选择&#xff0c;而是每一家模型的 API 规范完全不一样。OpenAI 用/v1/chat/completions&#xff0c;Anthropic 用/v1/messag…

作者头像 李华
网站建设 2026/9/9 8:57:23

ARM嵌入式固件启动与OTA工程化实战手册

1. 项目概述&#xff1a;这不是一篇“讲启动流程”的课&#xff0c;而是一份嵌入式固件工程师的现场作业手册 你点开这个标题&#xff0c;大概率不是为了听“CPU上电后PC指针怎么跳”这种教科书复述。你可能是刚被产线反馈“某批次设备冷机无法启动”&#xff0c;正在抓耳挠腮翻…

作者头像 李华
网站建设 2026/9/9 8:57:04

代码质量左移实战:2026主流工具横评与落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 8:56:25

MySQL WorkBench 8.0文件菜单与导航面板实操:打造通用版zip环境

简介&#xff1a;MySQL WorkBench 8.0 文件菜单导航通用版是一份针对数据库管理工具菜单栏的个性化配置资源&#xff0c;主要面向需要调整操作界面、简化操作流程或实现汉化的数据库管理员、开发人员及初学者。压缩包内仅含 1 个 xml 文件&#xff0c;大小仅 13KB&#xff0c;体…

作者头像 李华