在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>(); }
};
- Concepts 让
Factory只接受满足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 的新特性,工厂模式不再是“黑箱”,而是可读、可维护且编译时安全的代码。