在传统的 C++ 开发中,对象工厂模式往往伴随着臃肿的 switch-case 语句。每当新增一个派生类,开发者就不得不修改工厂源码。本文将带你利用 C++11 的高级特性——变长参数模板(Variadic Templates)、完美转发(Perfect Forwarding)与类型擦除(Type Erasure),构建一个既能自动注册,又支持运行时动态传参的顶级工厂模式。
一、 序言:从“腐烂”的简单工厂说起
传统的简单工厂模式通常是这样的:
1
2
3
4
5
6
7
8
| Message* create(int type) {
switch (type) {
case MSG_LOGIN: return new LoginMessage();
case MSG_CHAT: return new ChatMessage();
// 随着业务增长,这里将变成成百上千行
default: return nullptr;
}
}
|
这种模式有两个致命缺陷:
虽然此前我们通过“宏 + 静态辅助类”实现了自动注册,但那只能处理无参构造函数。在实际业务中,我们往往需要在创建实例时传入不同的参数(如用户 ID、等级、属性等)。
二、 核心技术栈:C++11 的三驾马车
要实现动态传参,我们需要解决:如何接收不定数量参数?如何保证参数不丢失属性地传递?如何存储签名完全不同的函数?
变长参数模板 (Variadic Templates):允许函数接收任意数量、任意类型的参数。
完美转发 (Perfect Forwarding):利用 std::forward 确保参数在多层传递中保持左值/右值属性,避免不必要的拷贝。
类型擦除 (Type Erasure):利用 void* 存储 Lambda 表达式生成的函数指针。
三、 架构设计:实现动态传参的黑魔法
我们的核心思路是:在注册时,不存储参数,而是存储一个支持特定签名的创建函数。
- 通用工厂模板类
我们定义一个泛型单例工厂,为每种基类族提供独立存储。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| template <typename Base>
class dynamic_factory {
public:
static dynamic_factory& get() {
static dynamic_factory instance;
return instance;
}
// 注册接口:Args 决定了构造函数的签名
template <typename T, typename... Args>
void register_class(const std::string& key) {
// 将 Lambda 转换为普通函数指针并存储为 void*
creators_[key] = reinterpret_cast<void*>(
+[](Args&&... args) -> Base* {
return new T(std::forward<Args>(args)...);
}
);
}
// 生产接口:Args 在调用时动态推导
template <typename... Args>
std::unique_ptr<Base> produce(const std::string& key, Args&&... args) {
if (creators_.find(key) == creators_.end()) return nullptr;
// 强转回对应的函数指针签名
using FuncType = Base* (*)(Args&&...);
auto func = reinterpret_cast<FuncType>(creators_[key]);
return std::unique_ptr<Base>(func(std::forward<Args>(args)...));
}
private:
std::map<std::string, void*> creators_;
dynamic_factory() = default;
};
|
- 自动注册辅助类与宏
为了实现“一行代码注册”,我们需要升级此前的宏:
1
2
3
4
5
6
7
8
9
10
| template <typename Base, typename T, typename... Args>
struct register_t {
register_t(const std::string& key) {
dynamic_factory<Base>::get().template register_class<T, Args...>(key);
}
};
#define REGISTER_CLASS_VNAME(T) reg_gen_##T##_
#define REGISTER_CLASS(Base, T, key, ...) \
static register_t<Base, T, ##**VA_ARGS**> REGISTER_CLASS_VNAME(T)(key);
|
四、 实战演练:角色扮演游戏中的动态生产
假设我们在开发一款 RPG 游戏,拥有不同的角色类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // 定义角色族群
struct Character {
virtual void info() = 0;
virtual ~Character() {}
};
struct Warrior : Character {
Warrior(std::string name, int level) : name_(name), level_(level) {}
void info() override { std::cout << "战士: " << name_ << " 等级: " << level_ << "\n"; }
std::string name_; int level_;
};
struct Mage : Character {
Mage(std::string name, std::string element) : name_(name), element_(element) {}
void info() override { std::cout << "法师: " << name_ << " 元素: " << element_ << "\n"; }
std::string name_; std::string element_;
};
// 注册战士,指定其构造参数为 (string, int)
REGISTER_CLASS(Character, Warrior, "warrior", std::string, int);
// 注册法师,指定其构造参数为 (string, string)
REGISTER_CLASS(Character, Mage, "mage", std::string, std::string);
int main() {
// 动态传入参数
auto p1 = dynamic_factory<Character>::get().produce("warrior", std::string("亚瑟"), 30);
auto p2 = dynamic_factory<Character>::get().produce("mage", std::string("甘道夫"), std::string("光辉"));
p1->info(); // 输出: 战士: 亚瑟 等级: 30
p2->info(); // 输出: 法师: 甘道夫 元素: 光辉
return 0;
}
|
五、 避坑指南与总结
注意事项:
参数匹配的严格性:生产时传入的参数类型必须与注册时的 Args… 完全一致。例如注册了 std::string,生产时传入 const char* 可能会导致函数指针转换后的调用失败(除非通过显式构造转换)。
内存安全:生产接口返回 unique_ptr,强制用户接受所有权,避免了原始指针导致的内存泄漏。
结语
通过现代 C++ 的黑魔法,我们将工厂模式推向了极致。这套方案彻底消除了 switch-case,将对象创建的决策权交给了配置(Key)与数据(Args),同时保持了代码的优雅与开闭原则的纯粹。