27、C++11/14/17新特性对设计模式的影响:智能指针与RAII、lambda表达式、移动语义、std::function与std::bind

说实话,C++11 之前的代码,写起来总有种「戴着镣铐跳舞」的感觉。那时候要实现一个回调,得写一堆函数对象;管理资源得小心翼翼,生怕忘了 delete。直到 C++11 带着一堆新特性砸下来,我才发现——原来设计模式可以写得这么优雅。

这一章,我们就聊聊这些新特性是怎么「重塑」经典设计模式的。我挑了几个对我影响最大的:智能指针、lambda、移动语义,还有 std::function 和 std::bind。每个都值得单独拿出来讲。

智能指针与 RAII:资源管理的「定海神针」

先说说智能指针。我记得刚入行那会儿,项目里到处都是裸指针,每次 new 完都要在脑子里记一笔「这里要 delete」。后来有一次线上服务 OOM,查了半天发现是某个异常分支忘了释放资源。从那以后,我对裸指针就有点 PTSD 了。

智能指针的出现,说白了就是把「谁拥有资源」这个问题彻底说清楚了。

  • std::unique_ptr:独占所有权,不能拷贝,只能移动。适合工厂模式返回的对象。
  • std::shared_ptr:共享所有权,引用计数。适合观察者模式、享元模式等需要共享资源的场景。
  • std::weak_ptr:解决循环引用。我经常用它来打破 shared_ptr 的环。

来看一个典型的工厂模式例子:

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

class ConcreteProduct : public Product {
public:
    void use() override {
        std::cout << "Using ConcreteProduct" << std::endl;
    }
};

// 工厂方法返回 unique_ptr,所有权明确
std::unique_ptr<Product> createProduct() {
    return std::make_unique<ConcreteProduct>();
}

// 使用
auto product = createProduct();
product->use();
// 离开作用域自动释放,不用操心 delete

你想想看,以前写工厂模式,返回裸指针还得在文档里写一句「调用者负责释放」。现在用 unique_ptr,语义自文档化,编译器还帮你检查——这感觉,舒服。

核心原则:能用 unique_ptr 就别用 shared_ptr。引用计数有开销,而且容易写出循环引用的坑。我见过一个项目,因为滥用 shared_ptr,内存占用比预期高了 30%。

我的习惯:在类的接口中,如果返回的是「新建的对象」,一律用 unique_ptr。如果调用方需要共享,再手动转成 shared_ptr。这样所有权流向清晰,代码也容易推理。

Lambda 表达式:让回调变得「随手就来」

Lambda 是我个人最爱的 C++11 特性之一。为什么?因为它让「策略模式」和「命令模式」的代码量直接砍半。

以前写策略模式,你得先定义一个抽象基类,然后派生一堆具体策略类。现在?一个 lambda 搞定:

// 传统策略模式
class Strategy {
public:
    virtual int execute(int a, int b) = 0;
};

class AddStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a + b; }
};

// Lambda 版本
auto addStrategy = [](int a, int b) { return a + b; };
auto multiplyStrategy = [](int a, int b) { return a * b; };

// 直接传入
void compute(std::function<int(int, int)> strategy, int x, int y) {
    std::cout << strategy(x, y) << std::endl;
}

compute(addStrategy, 3, 4);      // 输出 7
compute(multiplyStrategy, 3, 4); // 输出 12

你看,不需要继承,不需要虚函数,甚至不需要命名。lambda 本质上就是一个「匿名函数对象」,但它写起来比函数对象简洁太多了。

我在项目中经常用 lambda 配合 STL 算法。比如排序时自定义比较逻辑:

std::vector<Task> tasks = { ... };
std::sort(tasks.begin(), tasks.end(), 
    [](const Task& a, const Task& b) {
        return a.priority > b.priority; // 按优先级降序
    });

嗯,这里要注意:lambda 默认是按值捕获的。如果你要捕获引用,记得用 [&]。我曾经因为忘记写 &,导致捕获的变量是副本,改了也没用——排查了半天。

避坑指南:lambda 捕获 [this] 时要小心。如果 lambda 的生命周期超过了对象的生命周期,访问 this 就是未定义行为。我建议在捕获时明确写出 [this],不要用 [=] 隐式捕获,这样代码审查时一眼就能看出来。

移动语义:从「复制」到「转移」的思维转变

移动语义是 C++11 最底层的变革之一。它改变了我们写代码的方式——从「默认复制」变成了「默认移动」。这对设计模式的影响是深远的。

举个例子,在建造者模式中,我们经常需要传递大型对象。以前只能复制,现在可以移动:

class Builder {
    std::vector<std::string> parts;
public:
    Builder& addPart(std::string part) && {
        parts.push_back(std::move(part));
        return *this;
    }
    
    std::vector<std::string> build() && {
        return std::move(parts);
    }
};

// 使用
auto result = Builder{}
    .addPart("CPU")
    .addPart("GPU")
    .build();

注意看,addPart 和 build 都标记为 &&,表示只能在右值上调用。这意味着你不能意外地重复使用一个已经「掏空」的 Builder。这种设计在 C++11 之前是做不到的。

移动语义对工厂模式的影响也很大。以前工厂返回对象,要么返回指针(有所有权问题),要么返回值(有复制开销)。现在返回值就行了,编译器会优先选择移动构造:

class HeavyObject {
public:
    HeavyObject() = default;
    HeavyObject(HeavyObject&&) noexcept = default; // 移动构造
    // ...
};

HeavyObject createHeavy() {
    HeavyObject obj;
    // 配置 obj...
    return obj; // 编译器会尝试移动,甚至 RVO
}

说白了,移动语义让「值语义」重新变得有吸引力。以前为了性能不得不使用指针的地方,现在可以用值传递了。代码更安全,也更清晰。

关键点:移动语义不是「零开销」的,但通常比复制快得多。对于像 std::vector、std::string 这样的容器,移动只是交换几个指针,O(1) 复杂度。对于自定义类型,记得声明移动构造函数为 noexcept,否则 STL 容器在某些场景下会退化为复制。

std::function 与 std::bind:解耦的「万能胶」

std::function 是一个多态函数包装器。它可以存储任何可调用对象——函数指针、lambda、函数对象,甚至成员函数。这对设计模式中的「命令模式」和「策略模式」是革命性的。

以前实现命令模式,你得定义一个 Command 基类,然后派生具体命令。现在?

class Button {
    std::function<void()> onClick_;
public:
    void setOnClick(std::function<void()> handler) {
        onClick_ = std::move(handler);
    }
    
    void click() {
        if (onClick_) onClick_();
    }
};

// 使用
Button saveButton;
saveButton.setOnClick([]() {
    std::cout << "Saving..." << std::endl;
});

Button exitButton;
exitButton.setOnClick(std::bind(&Application::quit, &app));

你看,setOnClick 接受任何可调用对象。调用方可以传 lambda,也可以传 bind 表达式,甚至传一个函数指针。Button 类完全不需要知道具体实现——这就是解耦。

std::bind 现在用得少了,因为 lambda 基本能覆盖它的功能。但有些场景 bind 还是更方便,比如绑定成员函数时:

class Logger {
public:
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << std::endl;
    }
};

Logger logger;
auto boundLog = std::bind(&Logger::log, &logger, std::placeholders::_1);
boundLog("Hello"); // 等价于 logger.log("Hello")

我个人习惯是:能用 lambda 就用 lambda,可读性更好。bind 的占位符 _1、_2 多了之后,代码就变得晦涩了。只有在需要「延迟绑定参数」时才考虑 bind。

性能提示:std::function 有少量开销(虚函数调用级别的)。如果性能敏感,可以考虑直接用模板参数。但大多数业务场景下,这点开销可以忽略不计。我一般只在回调频繁触发的热点路径上才避免使用 std::function。

知识体系总览

下面这张图总结了 C++11/14/17 新特性对设计模式的影响脉络。你可以看到,每个新特性都对应着某些设计模式的「简化」或「重构」:

C++11/14/17 新特性对设计模式的影响 新特性核心 智能指针 + RAII Lambda 表达式 移动语义 std::function/bind 影响的设计模式 • 工厂模式(所有权明确) • 观察者模式(共享资源) 影响的设计模式 • 策略模式(无需派生) • 命令模式(内联定义) 影响的设计模式 • 建造者模式(链式调用) • 工厂模式(值返回) 影响的设计模式 • 命令模式 • 策略模式 核心思想 用更少的代码,表达更清晰的意图。让编译器帮你检查,让运行时更高效。

总结

C++11/14/17 的新特性不是孤立存在的。它们相互配合,让设计模式的实现变得更简洁、更安全、更高效。智能指针解决了资源管理的老大难问题;lambda 让回调定义变得随手就来;移动语义消除了不必要的复制;std::function 则提供了类型擦除的灵活性。

我个人觉得,这些新特性最大的价值不是「新语法」,而是「新思维」。它们让我们从「手动管理一切」的旧模式中解放出来,把更多精力放在业务逻辑和架构设计上。嗯,这才是现代 C++ 该有的样子。


公众号:蓝海资料掘金营,微信deep3321