哈喽,各位好!今天咱们来聊聊C++里的策略模式和模板,这俩哥们儿凑到一起,能玩出不少花样。咱们的目标是:在编译时,根据不同的需求,选择不同的算法实现。听起来是不是有点高大上?别怕,我会尽量用大白话,加上实际的代码例子,保证大家听得懂,学得会,还能乐在其中。
开胃小菜:策略模式是啥?
想象一下,你是一家咖啡馆的老板,卖咖啡的方式有很多种:你可以直接卖,可以打折卖,还可以搞买一送一。这些不同的卖咖啡的方式,就是不同的策略。
在编程里,策略模式就是把算法封装到一个个独立的类里,这些类都实现同一个接口。客户端(也就是调用这些算法的代码)可以根据需要,选择使用哪个策略。
主菜一:运行时策略模式(先热热身)
先来个传统的运行时策略模式,让大家熟悉一下基本概念。
#include <iostream>
#include <string>
// 策略接口:卖咖啡的策略
class CoffeeSellingStrategy {
public:
virtual double calculatePrice(double originalPrice) = 0;
virtual ~CoffeeSellingStrategy() {} // 确保析构函数是虚函数
};
// 具体策略:原价卖
class RegularPriceStrategy : public CoffeeSellingStrategy {
public:
double calculatePrice(double originalPrice) override {
return originalPrice;
}
};
// 具体策略:打折卖
class DiscountStrategy : public CoffeeSellingStrategy {
private:
double discountRate;
public:
DiscountStrategy(double rate) : discountRate(rate) {}
double calculatePrice(double originalPrice) override {
return originalPrice * (1 - discountRate);
}
};
// 具体策略:买一送一
class BuyOneGetOneStrategy : public CoffeeSellingStrategy {
public:
double calculatePrice(double originalPrice) override {
// 假设买一杯送一杯,实际价格相当于半价
return originalPrice * 0.5;
}
};
// 上下文:咖啡馆
class CoffeeShop {
private:
CoffeeSellingStrategy* strategy; // 指向策略对象的指针
public:
CoffeeShop(CoffeeSellingStrategy* sellingStrategy) : strategy(sellingStrategy) {}
void setSellingStrategy(CoffeeSellingStrategy* sellingStrategy) {
delete strategy; // 避免内存泄漏
strategy = sellingStrategy;
}
double sellCoffee(double originalPrice) {
return strategy->calculatePrice(originalPrice);
}
~CoffeeShop() {
delete strategy; // 别忘了释放内存
}
};
int main() {
// 创建咖啡馆,初始策略是原价卖
CoffeeShop coffeeShop(new RegularPriceStrategy());
// 卖一杯咖啡,原价10块
std::cout << "Regular price: " << coffeeShop.sellCoffee(10.0) << std::endl;
// 换策略:打八折
coffeeShop.setSellingStrategy(new DiscountStrategy(0.2));
std::cout << "Discounted price: " << coffeeShop.sellCoffee(10.0) << std::endl;
// 换策略:买一送一
coffeeShop.setSellingStrategy(new BuyOneGetOneStrategy());
std::cout << "Buy one get one: " << coffeeShop.sellCoffee(10.0) << std::endl;
return 0;
}
这个例子里,CoffeeSellingStrategy
是策略接口,RegularPriceStrategy
、DiscountStrategy
和BuyOneGetOneStrategy
是具体的策略实现。CoffeeShop
是上下文,它持有指向策略对象的指针,并在运行时动态地切换策略。
优点:
- 灵活性高,可以在运行时动态地切换算法。
缺点:
- 运行时有虚函数调用的开销。
- 需要使用指针和动态内存分配,增加复杂性。
- 策略对象需要手动管理生命周期(new 和 delete)。
主菜二:编译时策略模式(模板闪亮登场)
现在,咱们把模板请出来,实现编译时策略模式。目标是:在编译时,根据模板参数,选择不同的算法实现。
#include <iostream>
#include <string>
// 策略接口:卖咖啡的策略 (不需要是类)
namespace SellingStrategies {
// 原价卖
struct RegularPriceStrategy {
double calculatePrice(double originalPrice) {
return originalPrice;
}
};
// 打折卖
struct DiscountStrategy {
double discountRate;
DiscountStrategy(double rate) : discountRate(rate) {}
double calculatePrice(double originalPrice) {
return originalPrice * (1 - discountRate);
}
};
// 买一送一
struct BuyOneGetOneStrategy {
double calculatePrice(double originalPrice) {
// 假设买一杯送一杯,实际价格相当于半价
return originalPrice * 0.5;
}
};
}
// 上下文:咖啡馆 (模板类)
template <typename Strategy>
class CoffeeShop {
private:
Strategy strategy; // 策略对象作为成员变量
public:
//构造函数
CoffeeShop(Strategy s) : strategy(s) {}
double sellCoffee(double originalPrice) {
return strategy.calculatePrice(originalPrice);
}
};
int main() {
// 创建咖啡馆,使用原价策略
CoffeeShop<SellingStrategies::RegularPriceStrategy> coffeeShopRegular(SellingStrategies::RegularPriceStrategy{});
std::cout << "Regular price: " << coffeeShopRegular.sellCoffee(10.0) << std::endl;
// 创建咖啡馆,使用打折策略
CoffeeShop<SellingStrategies::DiscountStrategy> coffeeShopDiscount(SellingStrategies::DiscountStrategy{0.2});
std::cout << "Discounted price: " << coffeeShopDiscount.sellCoffee(10.0) << std::endl;
// 创建咖啡馆,使用买一送一策略
CoffeeShop<SellingStrategies::BuyOneGetOneStrategy> coffeeShopBuyOneGetOne(SellingStrategies::BuyOneGetOneStrategy{});
std::cout << "Buy one get one: " << coffeeShopBuyOneGetOne.sellCoffee(10.0) << std::endl;
return 0;
}
在这个例子里,CoffeeShop
是一个模板类,它的模板参数Strategy
指定了要使用的策略类型。在main
函数里,我们通过不同的模板参数,创建了使用不同策略的CoffeeShop
对象。
优点:
- 编译时确定策略,避免了运行时虚函数调用的开销,性能更好。
- 不需要使用指针和动态内存分配,代码更简洁,更容易维护。
- 策略对象的生命周期由
CoffeeShop
对象管理,避免了内存泄漏的风险。
缺点:
- 灵活性不如运行时策略模式,不能在运行时动态地切换策略。
- 代码膨胀:每个策略都会生成一份
CoffeeShop
的代码。
加餐:更高级的编译时策略模式(constexpr 和 if constexpr)
C++17 引入了 constexpr if
,它允许我们在编译时根据条件选择不同的代码路径。结合 constexpr
函数,我们可以实现更高级的编译时策略模式。
#include <iostream>
#include <string>
// 策略配置结构体
struct StrategyConfig {
bool useDiscount;
double discountRate;
bool useBuyOneGetOne;
};
// constexpr 函数:计算价格
constexpr double calculatePrice(double originalPrice, const StrategyConfig& config) {
double price = originalPrice;
if constexpr (config.useDiscount) {
price *= (1 - config.discountRate);
}
if constexpr (config.useBuyOneGetOne) {
price *= 0.5; // 买一送一
}
return price;
}
int main() {
// 配置策略:使用打八折
constexpr StrategyConfig config1 = {true, 0.2, false};
constexpr double price1 = calculatePrice(10.0, config1);
std::cout << "Discounted price: " << price1 << std::endl;
// 配置策略:使用买一送一
constexpr StrategyConfig config2 = {false, 0.0, true};
constexpr double price2 = calculatePrice(10.0, config2);
std::cout << "Buy one get one: " << price2 << std::endl;
// 配置策略:同时使用打八折和买一送一
constexpr StrategyConfig config3 = {true, 0.2, true};
constexpr double price3 = calculatePrice(10.0, config3);
std::cout << "Discounted and buy one get one: " << price3 << std::endl;
return 0;
}
在这个例子里,StrategyConfig
结构体定义了策略的配置信息。calculatePrice
函数是一个 constexpr
函数,它在编译时根据 StrategyConfig
的值,选择不同的计算价格的逻辑。if constexpr
确保了只有满足条件的代码块才会被编译。
优点:
- 完全在编译时确定策略,没有任何运行时开销。
- 代码更简洁,更容易阅读和维护。
- 可以使用更复杂的策略组合。
缺点:
- 策略的选择必须在编译时确定,不能在运行时动态地修改。
constexpr
函数有一些限制,比如不能使用循环和动态内存分配。
主菜三:使用 std::variant
实现编译时策略选择
C++17 引入了 std::variant
,它可以存储多种类型的值。我们可以利用 std::variant
来实现编译时策略选择。
#include <iostream>
#include <variant>
#include <functional> //std::function
// 策略接口 (使用函数对象)
namespace SellingStrategies {
// 原价卖
struct RegularPriceStrategy {
double operator()(double originalPrice) const {
return originalPrice;
}
};
// 打折卖
struct DiscountStrategy {
double discountRate;
DiscountStrategy(double rate) : discountRate(rate) {}
double operator()(double originalPrice) const {
return originalPrice * (1 - discountRate);
}
};
// 买一送一
struct BuyOneGetOneStrategy {
double operator()(double originalPrice) const {
return originalPrice * 0.5;
}
};
}
// 上下文:咖啡馆 (使用 std::variant 存储策略)
class CoffeeShop {
private:
std::variant<SellingStrategies::RegularPriceStrategy,
SellingStrategies::DiscountStrategy,
SellingStrategies::BuyOneGetOneStrategy> strategy;
public:
//构造函数,传入variant,确定策略类型
CoffeeShop(std::variant<SellingStrategies::RegularPriceStrategy,
SellingStrategies::DiscountStrategy,
SellingStrategies::BuyOneGetOneStrategy> s) : strategy(s) {}
double sellCoffee(double originalPrice) {
// 使用 std::visit 调用相应的策略函数
return std::visit(
[&](auto&& arg) -> double {
return arg(originalPrice);
},
strategy);
}
};
int main() {
// 创建咖啡馆,使用原价策略
CoffeeShop coffeeShopRegular(SellingStrategies::RegularPriceStrategy{});
std::cout << "Regular price: " << coffeeShopRegular.sellCoffee(10.0) << std::endl;
// 创建咖啡馆,使用打折策略
CoffeeShop coffeeShopDiscount(SellingStrategies::DiscountStrategy{0.2});
std::cout << "Discounted price: " << coffeeShopDiscount.sellCoffee(10.0) << std::endl;
// 创建咖啡馆,使用买一送一策略
CoffeeShop coffeeShopBuyOneGetOne(SellingStrategies::BuyOneGetOneStrategy{});
std::cout << "Buy one get one: " << coffeeShopBuyOneGetOne.sellCoffee(10.0) << std::endl;
return 0;
}
在这个例子中,std::variant
存储了所有可能的策略类型。 std::visit
是一个强大的工具,它允许我们根据 std::variant
中存储的实际类型,调用相应的策略函数。
优点:
- 编译时确定策略类型,避免了虚函数调用开销。
- 可以在运行时改变策略,但只能在预定义的策略类型之间切换(类型安全)。
- 代码清晰,易于理解。
缺点:
std::variant
的大小是所有可能类型中最大的类型的大小,可能会占用较多内存。- 需要在编译时知道所有可能的策略类型。
总结:选择合适的策略模式
特性 | 运行时策略模式 | 编译时策略模式 (模板) | 编译时策略模式 (constexpr) | 编译时策略模式 (std::variant) |
---|---|---|---|---|
灵活性 | 高 (运行时切换) | 低 (编译时确定) | 低 (编译时确定) | 中 (预定义类型之间切换) |
性能 | 低 (虚函数调用) | 高 (无虚函数调用) | 最高 (完全编译时) | 中 (无虚函数调用,std::visit) |
代码复杂度 | 中 (指针,动态内存) | 低 (无指针) | 低 (constexpr 函数) | 中 (std::variant, std::visit) |
内存占用 | 动态分配 | 静态分配 | 静态分配 | 静态分配 (variant 大小) |
适用场景 | 需要动态切换算法 | 算法在编译时已知 | 算法配置在编译时已知 | 算法类型有限且需要在运行时选择 |
选择哪种策略模式,取决于你的具体需求。
- 如果需要在运行时动态地切换算法,运行时策略模式是首选。
- 如果算法在编译时已知,并且对性能要求很高,编译时策略模式 (模板) 是更好的选择。
- 如果需要根据编译时已知的配置信息选择算法,
constexpr
和if constexpr
是一个强大的工具。 - 如果需要在运行时改变策略,但只能在预定义的策略类型之间切换,
std::variant
是一个不错的选择。
餐后甜点:策略模式和模板的结合应用
策略模式和模板可以结合使用,创造出更灵活、更强大的设计。例如,你可以使用模板来实现策略接口,然后使用运行时策略模式来动态地切换不同的模板策略。
#include <iostream>
#include <string>
// 模板策略接口
template <typename T>
class Strategy {
public:
virtual T calculate(T value) = 0;
virtual ~Strategy() {}
};
// 具体策略 A
template <typename T>
class StrategyA : public Strategy<T> {
public:
T calculate(T value) override {
return value * 2;
}
};
// 具体策略 B
template <typename T>
class StrategyB : public Strategy<T> {
public:
T calculate(T value) override {
return value + 1;
}
};
// 上下文
template <typename T>
class Context {
private:
Strategy<T>* strategy;
public:
Context(Strategy<T>* strategy) : strategy(strategy) {}
void setStrategy(Strategy<T>* strategy) {
delete this->strategy;
this->strategy = strategy;
}
T executeStrategy(T value) {
return strategy->calculate(value);
}
~Context() {
delete strategy;
}
};
int main() {
// 创建上下文,初始策略是 StrategyA<int>
Context<int> context(new StrategyA<int>());
std::cout << "Strategy A: " << context.executeStrategy(5) << std::endl;
// 切换策略到 StrategyB<int>
context.setStrategy(new StrategyB<int>());
std::cout << "Strategy B: " << context.executeStrategy(5) << std::endl;
return 0;
}
这个例子中,Strategy
是一个模板类,它定义了策略接口。StrategyA
和 StrategyB
是具体的策略实现,它们也都是模板类。Context
是上下文,它持有指向 Strategy
对象的指针,并在运行时动态地切换策略。
总结的总结:
策略模式和模板是C++里非常强大的工具,它们可以帮助我们编写更灵活、更可维护、更高效的代码。希望今天的讲解能够帮助大家更好地理解和使用这两种设计模式。记住,没有银弹,选择最适合你需求的策略才是王道! 好了,今天的分享就到这里,希望大家有所收获! 拜拜!