利用C++20 Concepts实现类型安全的工厂函数

在C++20之前,工厂函数往往需要通过基类指针或模板特化来返回不同类型的对象。这样做会导致运行时类型检查、显式的dynamic_cast,甚至在编译期无法捕获类型错误。随着C++20引入的 Concepts 和模板变量,能够在编译期强制类型约束,从而让工厂函数既安全又易于维护。

1. 传统工厂的痛点

class Shape { public: virtual ~Shape() = default; virtual void draw() const = 0; };
class Circle : public Shape { void draw() const override { /* ... */ } };
class Square : public Shape { void draw() const override { /* ... */ } };

std::unique_ptr <Shape> createShape(const std::string& type) {
    if (type == "circle") return std::make_unique <Circle>();
    if (type == "square") return std::make_unique <Square>();
    throw std::invalid_argument("unknown shape");
}
  • 需要维护字符串与类的映射,易出错。
  • 调用者只能得到基类指针,导致多态带来的性能成本。
  • 编译期无法捕获非法类型。

2. Concepts 与模板变量的力量

#include <concepts>
#include <string_view>
#include <memory>
#include <stdexcept>

template<typename T>
concept ShapeConcept = requires(T s) {
    { s.draw() } -> std::same_as <void>;
};

template<ShapeConcept T>
struct Factory {
    static std::unique_ptr <T> create() { return std::make_unique<T>(); }
};
  • ConceptsFactory 只接受满足 draw() 成员函数的类型。
  • 模板变量 可以在编译期确定工厂的返回类型。

3. 以类型标签实现编译期映射

enum class ShapeType { Circle, Square };

template<ShapeType S>
struct ShapeMaker;

template<>
struct ShapeMaker<ShapeType::Circle> {
    static std::unique_ptr <Circle> create() { return std::make_unique<Circle>(); }
};

template<>
struct ShapeMaker<ShapeType::Square> {
    static std::unique_ptr <Square> create() { return std::make_unique<Square>(); }
};

template<ShapeType S>
std::unique_ptr <Shape> createShape() {
    return ShapeMaker <S>::create();
}
  • 调用者使用 createShape<ShapeType::Circle>(),编译器在编译期决定返回 Circle 的工厂实例。
  • 没有字符串映射,错误可在编译期捕获。

4. 组合 Concepts 与类型标签

template<ShapeType S>
requires std::same_as<std::remove_cvref_t<decltype(ShapeMaker<S>::create())>, Shape>
std::unique_ptr <Shape> createShape() {
    return ShapeMaker <S>::create();
}
  • requires 进一步限制 ShapeMaker <S>::create() 的返回类型必须是 Shape 的派生。
  • 这保证了所有工厂都遵循统一接口。

5. 运行时灵活性:字符串到标签的映射

虽然我们倾向于纯编译期映射,但有时需要在运行时根据用户输入选择类型。我们可以把字符串映射到 ShapeType,然后使用编译期工厂。

ShapeType strToType(const std::string_view sv) {
    if (sv == "circle") return ShapeType::Circle;
    if (sv == "square") return ShapeType::Square;
    throw std::invalid_argument("unknown shape");
}

std::unique_ptr <Shape> createShapeRuntime(const std::string_view name) {
    switch (strToType(name)) {
        case ShapeType::Circle: return createShape<ShapeType::Circle>();
        case ShapeType::Square: return createShape<ShapeType::Square>();
    }
    // unreachable
}
  • strToType 负责字符串到枚举的转换。
  • createShapeRuntime 只在运行时决定枚举值,后续的工厂调用仍然是编译期生成的。

6. 总结

  • Concepts 让我们在编译期约束工厂的输入和输出类型,避免了运行时错误。
  • 模板变量类型标签 的结合,提供了既灵活又安全的工厂实现。
  • 通过枚举映射实现字符串输入的转换,既保留了运行时灵活性,又保持了编译期检查。

利用 C++20 的新特性,工厂模式不再是“黑箱”,而是可读、可维护且编译时安全的代码。

发表评论