Effective C++ 条款17:以独立语句将 newed 对象置入智能指针

Effective C++ 条款17:以独立语句将 newed 对象置入智能指针

🎯 核心观点:以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,可能导致难以察觉的资源泄漏。


一、一个看似无害的陷阱

让我们从一个常见的函数调用开始:

cpp 复制代码
#include <memory>
#include <iostream>

class Widget {
public:
    Widget() { std::cout << "Widget 构造\n"; }
    ~Widget() { std::cout << "Widget 析构\n"; }
};

void processWidget(std::shared_ptr<Widget> pw, int priority);
int computePriority();

// 看似合理的调用方式
void doWork() {
    processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());
}

这段代码有问题吗?表面上看,new Widget() 的结果被立即传入了 shared_ptr 的构造函数,然后传给 processWidget。但这里隐藏着一个致命的异常安全隐患


二、编译器视角:操作序列的不确定性

2.1 C++ 的求值顺序

在深入问题之前,我们需要理解一个关键事实:

⚠️ C++ 标准不保证函数参数之间的求值顺序(直到 C++17 才对某些情况做出规定)。

在 C++17 之前,编译器在编译 processWidget(std::shared_ptr<Widget>(new Widget()), computePriority()) 时,以下操作的执行顺序是不确定的

  1. 执行 new Widget()
  2. 调用 std::shared_ptr<Widget> 的构造函数
  3. 调用 computePriority()

编译器可能按照以下顺序执行(这是合法的):

复制代码
1. new Widget()        → 返回裸指针
2. computePriority()   → 可能抛出异常!
3. shared_ptr 构造     → 如果步骤2抛异常,这一步永远不会执行

2.2 灾难场景还原

假设 computePriority() 在运行时抛出了异常:

cpp 复制代码
int computePriority() {
    // 某些条件下抛出异常
    throw std::runtime_error("优先级计算失败");
    return 42;
}

void doWork() {
    // 潜在的资源泄漏!
    processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());
}

执行流程可能是:

步骤 操作 结果
1 new Widget() 分配内存,调用构造函数,返回裸指针
2 computePriority() 抛出异常
3 shared_ptr 构造 永远不会执行

此时,new Widget() 返回的裸指针永远丢失了 ------没有智能指针接管它,也没有代码能够 delete 它。这就是资源泄漏

💀 这种泄漏特别危险,因为:

  • 代码看起来"正确"
  • 编译器不会报错
  • 只有在特定异常触发时才会发生
  • 极难在测试中发现

三、解决方案:独立语句

3.1 正确的写法

new 和智能指针的构造放在一条独立的语句中:

cpp 复制代码
void doWorkSafe() {
    // ✅ 独立语句:newed 对象立即被置入智能指针
    std::shared_ptr<Widget> pw(new Widget());
    
    // 现在即使 computePriority() 抛异常,pw 的析构函数也会正确释放 Widget
    processWidget(pw, computePriority());
}

为什么这样更安全?

因为 C++ 保证:在一条语句执行完毕之前,编译器不会插入其他操作 。一旦 std::shared_ptr<Widget> pw(new Widget()); 这条语句完成,Widget 对象就已经被智能指针安全接管了。

3.2 C++11 的更佳方案:std::make_shared

在 C++11 及以后,最佳实践 是使用 std::make_shared

cpp 复制代码
void doWorkBest() {
    // ✅ 最佳实践:使用 make_shared,更安全、更高效
    auto pw = std::make_shared<Widget>();
    processWidget(pw, computePriority());
}

std::make_shared 的优势:

优势 说明
异常安全 Widget 的创建和 shared_ptr 的管理是原子操作
性能更好 只需一次内存分配(控制块 + 对象在同一内存块中)
代码简洁 不需要显式 new
防止泄漏 彻底杜绝裸指针暴露的机会

3.3 对比总结

cpp 复制代码
// ❌ 危险:参数内部完成 new 和智能指针构造
processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());

// ✅ 安全:独立语句
std::shared_ptr<Widget> pw(new Widget());
processWidget(pw, computePriority());

// ✅✅ 最佳:使用 make_shared
auto pw = std::make_shared<Widget>();
processWidget(pw, computePriority());

四、实际应用场景

4.1 场景:GUI 框架中的控件创建

cpp 复制代码
#include <memory>
#include <stdexcept>

class Button {
public:
    explicit Button(const std::string& label) { /* ... */ }
};

class Window {
public:
    void addButton(std::shared_ptr<Button> btn, int zOrder);
};

int calculateZOrder();  // 可能抛出异常

// ❌ 危险的控件添加
void setupWindowDangerous(Window& window) {
    window.addButton(std::shared_ptr<Button>(new Button("OK")), calculateZOrder());
}

// ✅ 安全的控件添加
void setupWindowSafe(Window& window) {
    auto btn = std::make_shared<Button>("OK");
    window.addButton(btn, calculateZOrder());
}

4.2 场景:数据库连接池

cpp 复制代码
#include <memory>
#include <string>

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connStr) {
        // 建立连接,可能分配大量资源
    }
    ~DatabaseConnection() {
        // 关闭连接,释放资源
    }
};

std::string buildConnectionString();  // 可能因配置错误抛出异常

class ConnectionPool {
public:
    void addConnection(std::shared_ptr<DatabaseConnection> conn, int priority);
};

// ❌ 危险:如果 buildConnectionString 抛异常,数据库连接泄漏
void initializePoolDangerous(ConnectionPool& pool) {
    pool.addConnection(
        std::shared_ptr<DatabaseConnection>(new DatabaseConnection("initial")),
        buildConnectionString().length()
    );
}

// ✅ 安全:分步执行
void initializePoolSafe(ConnectionPool& pool) {
    auto conn = std::make_shared<DatabaseConnection>("initial");
    auto connStr = buildConnectionString();  // 即使这里抛异常,conn 也会被正确释放
    pool.addConnection(conn, connStr.length());
}

4.3 场景:多线程环境中的资源创建

在多线程环境下,这个问题更加微妙:

cpp 复制代码
#include <memory>
#include <thread>
#include <vector>

class Task {
public:
    void execute();
};

int fetchTaskPriority();  // 可能涉及网络请求,可能抛异常

// ❌ 在多线程中传递参数时尤其危险
void dispatchTaskDangerous() {
    std::thread t(
        [](std::shared_ptr<Task> task, int priority) {
            task->execute();
        },
        std::shared_ptr<Task>(new Task()),  // 潜在的泄漏点
        fetchTaskPriority()                 // 可能抛异常
    );
    t.detach();
}

// ✅ 安全版本
void dispatchTaskSafe() {
    auto task = std::make_shared<Task>();
    int priority = fetchTaskPriority();
    
    std::thread t(
        [](std::shared_ptr<Task> t, int p) {
            t->execute();
        },
        task,
        priority
    );
    t.detach();
}

五、深入理解:为什么编译器会这样?

5.1 编译器的优化自由

C++ 标准赋予编译器很大的自由度来优化函数参数的求值顺序。这种设计是为了:

  • 允许不同架构的最优代码生成
  • 支持各种编译器优化策略
  • 保持语言的灵活性

但这也意味着程序员必须对序列点(sequence points,C++11 后称为 sequenced-before 关系)有清晰的理解。

5.2 C++17 的改进

好消息是,C++17 引入了更严格的求值顺序规则:

在 C++17 中,函数参数的求值顺序仍然不完全确定,但某些操作被保证为顺序执行。

然而,即使如此,显式使用独立语句仍然是最佳实践,因为它:

  1. 让代码意图更清晰
  2. 兼容 C++11/14 代码库
  3. 避免依赖复杂的求值顺序规则
  4. 更容易被代码审查者理解

六、unique_ptr 同样适用

虽然条款示例使用了 shared_ptr,但同样的原则完全适用于 unique_ptr

cpp 复制代码
#include <memory>

class Resource {
public:
    Resource() = default;
    void process();
};

void useResource(std::unique_ptr<Resource> res, int config);
int loadConfig();  // 可能抛异常

// ❌ 危险
void prepareDangerous() {
    useResource(std::unique_ptr<Resource>(new Resource()), loadConfig());
}

// ✅ 安全
void prepareSafe() {
    auto res = std::make_unique<Resource>();
    useResource(std::move(res), loadConfig());
}

💡 注意unique_ptr 不可拷贝,所以传递时需要 std::move。但创建它时仍然应该使用独立语句或 std::make_unique(C++14 起可用)。


七、总结

要点 说明
核心规则 以独立语句将 newed 对象置入智能指针
根本原因 C++ 不保证函数参数的求值顺序
潜在风险 异常抛出时,裸指针未被智能指针接管,导致资源泄漏
最佳实践 使用 std::make_shared / std::make_unique,彻底避免裸指针
兼容性 独立语句写法兼容所有 C++ 版本

📌 一句话记住 :不要让 new 和智能指针的构造之间"夹"着可能抛异常的代码。要么用独立语句,要么直接用 make_shared/make_unique


八、延伸阅读

  • Effective C++ 条款13:以对象管理资源
  • Effective C++ 条款16:成对使用 new 和 delete 时要采取相同形式
  • Effective C++ 条款18:让接口容易被正确使用,不易被误用
  • C++ Core Guidelines:ES.60 - Avoid new and delete outside resource management functions

如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬!你的支持是我持续创作的动力!

相关推荐
飞天狗1111 小时前
零基础JavaWeb入门——第2课:让网页“活”起来 —— JSP是什么?
java·开发语言·前端·后端·web
RisunJan2 小时前
Linux命令-pgrep (通过进程名查找进程 ID)
linux·运维
梦@_@境2 小时前
面向 Spring Boot 的可观测业务流程编排引擎
java·spring boot·后端
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【77】执行取消
java·人工智能·spring
醇氧2 小时前
【Linux】Java 服务生产级部署指南:实现常驻后台、开机自启与系统服务化管理
java·开发语言
信创工程师-小杨2 小时前
Linux内网环境如何解决依赖的问题
linux·运维·服务器
设计师小聂!2 小时前
宝塔 Linux 面板保姆级教程
linux·mysql·开源·运维开发
凡人叶枫3 小时前
Effective C++ 条款16:成对使用 new 和 delete 时要采取相同形式
开发语言·c++·effective c++
JAVA面经实录9173 小时前
Netty 全套系统化学习文档(零基础到高阶面试完整版)
java·后端