28. 完美转发在工厂模式中如何应用?
工厂模式,说白了就是帮你“造对象”的。你给它一堆参数,它给你返回一个成品。但C++里有个麻烦事——参数怎么传?左值?右值?const?非const?传统写法你得写一堆重载,累死人。
完美转发就是来救场的。它让工厂函数能“原封不动”地把参数转发给构造函数。嗯,这里有个关键点:参数的类型信息(左值/右值、const等)必须一丝不差地保留。否则,你传了个右值进去,结果被当成左值处理,移动语义就废了。
传统工厂的痛点
我刚开始写工厂时,也踩过这个坑。你想想看,一个工厂要支持多种构造方式,比如:
class Widget {
public:
Widget(int id) : id_(id) {}
Widget(const std::string& name) : name_(name) {}
Widget(std::string&& name) : name_(std::move(name)) {}
private:
int id_ = 0;
std::string name_;
};
传统工厂你得这么写:
template<typename T>
std::unique_ptr<T> create(int id) {
return std::make_unique<T>(id);
}
template<typename T>
std::unique_ptr<T> create(const std::string& name) {
return std::make_unique<T>(name);
}
template<typename T>
std::unique_ptr<T> create(std::string&& name) {
return std::make_unique<T>(std::move(name));
}
看到没?三个重载!如果参数类型再多几个,比如加上double、vector、自定义类型……那代码量直接爆炸。而且你还要为每种组合写重载,维护起来简直是噩梦。
完美转发工厂:一劳永逸
用完美转发,一个模板搞定所有:
template<typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
就这么简单。你传左值,它调左值版本;传右值,它调右值版本。const、volatile什么的也都保留。我个人习惯把这种写法叫做“万能工厂”。
举个例子:
std::string s = "hello";
auto w1 = create<Widget>(42); // 右值,完美转发
auto w2 = create<Widget>(s); // 左值,完美转发
auto w3 = create<Widget>(std::move(s)); // 右值引用,完美转发
三个调用,一个模板函数搞定。代码量从3个重载减到1个,而且可读性还更高。
避坑指南:引用折叠与转发语义
我曾经在项目里犯过一个低级错误——忘了用std::forward,直接用了std::move。结果呢?左值参数也被移动了,导致后续代码拿到的是空对象。嗯,这里要注意:
- 必须用
std::forward,而不是std::move。前者是条件转发,后者是无条件转成右值。 - 参数类型必须是
Args&&(转发引用),而不是const Args&或Args&。只有转发引用才能保留值类别。
来看个错误示范:
// 错误!参数是const引用,丢失了右值信息
template<typename T, typename... Args>
std::unique_ptr<T> create(const Args&... args) {
return std::make_unique<T>(args...); // 永远都是左值拷贝
}
你传右值进去,它还是拷贝。性能直接打回原形。
工厂模式中的高级应用
完美转发在工厂里还能玩出更多花样。比如,支持多态创建:
template<typename Base, typename Derived, typename... Args>
std::unique_ptr<Base> createDerived(Args&&... args) {
return std::make_unique<Derived>(std::forward<Args>(args)...);
}
// 使用
auto obj = createDerived<Base, Derived>(42, "test");
再比如,结合std::function做注册式工厂:
class Factory {
public:
template<typename T, typename... Args>
void registerType(const std::string& key) {
creators_[key] = [](Args&&... args) -> std::unique_ptr<void> {
return std::make_unique<T>(std::forward<Args>(args)...);
};
}
template<typename... Args>
std::unique_ptr<void> create(const std::string& key, Args&&... args) {
auto it = creators_.find(key);
if (it != creators_.end()) {
return it->second(std::forward<Args>(args)...);
}
return nullptr;
}
private:
std::unordered_map<std::string,
std::function<std::unique_ptr<void>(Args...)>> creators_;
};
这个模式我实际用过。在某个插件系统里,需要动态创建不同类型的对象。每个插件注册自己的类型和构造参数,工厂统一管理。完美转发保证了参数传递的零开销。
性能对比:完美转发 vs 传统重载
| 场景 | 传统重载 | 完美转发 |
|---|---|---|
| 代码量 | N个重载 | 1个模板 |
| 维护成本 | 高(每加一个类型都要加重载) | 低(自动适配) |
| 性能 | 无额外开销 | 无额外开销(编译期展开) |
| 右值支持 | 需要显式写右值重载 | 自动支持 |
| 可读性 | 差(一堆重载) | 好(一个模板) |
你看,完美转发在各方面都占优。唯一的“代价”是模板编译时间稍长,但这点开销跟维护成本比起来,不值一提。
核心逻辑图
下面这张图展示了完美转发在工厂模式中的完整流程:
这张图的核心思想:调用方传什么类型,工厂就原封不动地转发给构造函数。左值还是左值,右值还是右值,const还是const。引用折叠规则保证了这一点。
实际项目中的经验
我在一个游戏引擎项目里用过这个模式。当时要创建各种游戏对象——角色、道具、场景等。每个对象的构造参数都不一样,有的需要位置坐标,有的需要纹理路径,有的需要碰撞体数据。
如果用传统重载,那代码量……我不敢想。用了完美转发工厂后,整个创建模块就几十行代码。后来加新对象类型,只需要注册一下,工厂自动适配。维护成本几乎为零。
核心要点:
- 完美转发工厂 = 模板 + 转发引用 + std::forward
- 一个工厂函数替代N个重载
- 零性能开销,编译期完成所有类型推导
- 左值/右值/const 全部自动保留
个人建议:如果你在写工厂模式,而且参数类型不确定或可能变化,直接用完美转发。别犹豫。代码量减少50%以上,可维护性提升一个档次。
注意:完美转发不是万能的。如果构造函数有多个重载且参数类型冲突(比如int和long),模板推导可能失败。这时候需要SFINAE或concept来约束。不过,90%的场景下,完美转发工厂已经够用了。
最后说一句:完美转发工厂不是银弹,但它确实是C++工厂模式里最优雅的解决方案之一。你想想看,一个模板函数,搞定所有参数组合,还能保持零开销抽象——这不就是我们写C++追求的吗?