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个模板
维护成本 高(每加一个类型都要加重载) 低(自动适配)
性能 无额外开销 无额外开销(编译期展开)
右值支持 需要显式写右值重载 自动支持
可读性 差(一堆重载) 好(一个模板)

你看,完美转发在各方面都占优。唯一的“代价”是模板编译时间稍长,但这点开销跟维护成本比起来,不值一提。

核心逻辑图

下面这张图展示了完美转发在工厂模式中的完整流程:

完美转发工厂模式核心流程 调用方 create<Widget>(42) Args&&... 工厂函数 std::forward<Args>(args)... 完美转发 构造函数 Widget(int) 引用折叠与类型保留 调用方参数类型 int x = 42; create<Widget>(x) → Args 推导为 int& → std::forward<int&> 返回左值 调用方参数类型 create<Widget>(42) → Args 推导为 int → std::forward<int> 返回右值

这张图的核心思想:调用方传什么类型,工厂就原封不动地转发给构造函数。左值还是左值,右值还是右值,const还是const。引用折叠规则保证了这一点。

实际项目中的经验

我在一个游戏引擎项目里用过这个模式。当时要创建各种游戏对象——角色、道具、场景等。每个对象的构造参数都不一样,有的需要位置坐标,有的需要纹理路径,有的需要碰撞体数据。

如果用传统重载,那代码量……我不敢想。用了完美转发工厂后,整个创建模块就几十行代码。后来加新对象类型,只需要注册一下,工厂自动适配。维护成本几乎为零。

核心要点:

  • 完美转发工厂 = 模板 + 转发引用 + std::forward
  • 一个工厂函数替代N个重载
  • 零性能开销,编译期完成所有类型推导
  • 左值/右值/const 全部自动保留

个人建议:如果你在写工厂模式,而且参数类型不确定或可能变化,直接用完美转发。别犹豫。代码量减少50%以上,可维护性提升一个档次。

注意:完美转发不是万能的。如果构造函数有多个重载且参数类型冲突(比如int和long),模板推导可能失败。这时候需要SFINAE或concept来约束。不过,90%的场景下,完美转发工厂已经够用了。

最后说一句:完美转发工厂不是银弹,但它确实是C++工厂模式里最优雅的解决方案之一。你想想看,一个模板函数,搞定所有参数组合,还能保持零开销抽象——这不就是我们写C++追求的吗?