C++ 策略模式与模板:编译时选择不同算法实现

哈喽,各位好!今天咱们来聊聊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是策略接口,RegularPriceStrategyDiscountStrategyBuyOneGetOneStrategy是具体的策略实现。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 大小)
适用场景 需要动态切换算法 算法在编译时已知 算法配置在编译时已知 算法类型有限且需要在运行时选择

选择哪种策略模式,取决于你的具体需求。

  • 如果需要在运行时动态地切换算法,运行时策略模式是首选。
  • 如果算法在编译时已知,并且对性能要求很高,编译时策略模式 (模板) 是更好的选择。
  • 如果需要根据编译时已知的配置信息选择算法,constexprif 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 是一个模板类,它定义了策略接口。StrategyAStrategyB 是具体的策略实现,它们也都是模板类。Context 是上下文,它持有指向 Strategy 对象的指针,并在运行时动态地切换策略。

总结的总结:

策略模式和模板是C++里非常强大的工具,它们可以帮助我们编写更灵活、更可维护、更高效的代码。希望今天的讲解能够帮助大家更好地理解和使用这两种设计模式。记住,没有银弹,选择最适合你需求的策略才是王道! 好了,今天的分享就到这里,希望大家有所收获! 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注