C++14 之 std::make_unique

C++11 引入了智能指针三件套,却唯独漏掉了一个关键角色——make_unique。结果就是你得在代码里到处写 new,一不小心就埋下异常安全的定时炸弹。C++14 把这个缺口补上了:一个函数调用,搞定内存分配、对象构造和智能指针绑定,干净利落。


一、C++11 智能指针的遗留问题

1.1 有 make_shared,却没 make_unique

C++11 提供了 std::make_shared,让你一行代码创建 shared_ptr

auto sp = std::make_shared<Widget>(args...);  // ✅ 安全、高效、优雅

unique_ptr 呢?没有。你只能手动 new

auto up = std::unique_ptr<Widget>(new Widget(args...));  // 😐 能用,但不够优雅

看似小事,实则暗藏隐患。

1.2 隐式 new 的异常安全问题

看这个看似无害的函数调用:

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

当你这样调用时:

processWidget(std::unique_ptr<Widget>(new Widget), computePriority());

C++ 标准不保证参数的求值顺序。编译器可能按以下顺序执行:

① new Widget              // 堆分配 + 构造对象
② computePriority()       // 计算优先级
③ unique_ptr 构造          // 接管裸指针

如果步骤②抛出异常(比如内存不足),步骤①分配的内存已经泄漏了——因为 unique_ptr 还没接管这个裸指针。

这不是理论上的极端情况。在大型项目中,computePriority() 可能涉及 I/O、网络请求或复杂的计算,抛异常的概率并不低。

1.3 代码冗余

手动 new 还带来了不必要的冗余:

// 你明明知道类型是 Widget,却得写两遍
std::unique_ptr<Widget> pw(new Widget(42));
//          Widget ↑                    ↑ Widget 又写了一遍

重复即错误之源。类型名出现两次,意味着改类型时要改两处,漏改就会编译失败。

std::make_unique 一次性解决了所有这些问题。


二、make_unique 详解

2.1 核心语法

auto ptr = std::make_unique<T>(args...);

就这么简单。一行搞定类型声明、内存分配、对象构造和智能指针绑定。

等价于:

std::unique_ptr<T> ptr(new T(args...));

但更安全、更简洁。

2.2 为什么更安全

make_unique 内部会先完成内存分配和对象构造,然后直接将结果包装为 unique_ptr 返回。整个过程没有裸指针中间态,不存在"裸指针悬空"的风险:

make_unique 的执行流程:

  make_unique<Widget>(42)
         │
         ▼
  ┌──────────────────┐
  │ new Widget(42)   │  ← 分配 + 构造
  └────────┬─────────┘
           │
           ▼
  ┌──────────────────┐
  │ unique_ptr 包装   │  ← 立即接管
  └──────────────────┘
           │
           ▼
      返回给调用者

  没有裸指针中间态 → 无泄漏风险 ✅

即使 make_unique 内部的构造过程抛出异常,new 分配的内存也会被正确释放(因为异常在 new 返回之前发生)。

2.3 与直接 new 的对比

对比项 make_unique<T>(args...) unique_ptr<T>(new T(args...))
异常安全 ✅ 安全 ❌ 可能泄漏
类型冗余 ✅ 只写一次 ❌ 类型写两遍
代码简洁度 ⭐⭐⭐ ⭐⭐
可读性
裸指针暴露 ❌ 无 ⚠️ 有中间态

三、使用场景

3.1 创建独占所有权的对象

最基本的用法——需要一个独占管理资源的对象时:

#include <iostream>
#include <memory>
#include <string>

class Connection {
public:
    Connection(const std::string& url) : url_(url) {
        std::cout << "连接到 " << url_ << std::endl;
    }
    ~Connection() {
        std::cout << "断开 " << url_ << std::endl;
    }
    void execute(const std::string& cmd) {
        std::cout << "[" << url_ << "] 执行: " << cmd << std::endl;
    }
private:
    std::string url_;
};

int main() {
    // 一行创建,自动管理生命周期
    auto conn = std::make_unique<Connection>("db://localhost:5432");
    conn->execute("SELECT * FROM users");

    // 离开作用域时自动断开连接
    return 0;
}

输出:

连接到 db://localhost:5432
[db://localhost:5432] 执行: SELECT * FROM users
断开 db://localhost:5432

无需手动 delete,无需担心异常路径上的资源泄漏。

3.2 工厂模式

工厂函数返回 unique_ptr 是最常见的场景。make_unique 让工厂函数变得极其简洁:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

// 基类
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
    virtual double area() const = 0;
};

// 派生类
class Circle : public Shape {
public:
    explicit Circle(double r) : radius_(r) {}
    void draw() const override {
        std::cout << "画圆(r=" << radius_ << ")" << std::endl;
    }
    double area() const override { return 3.14159265 * radius_ * radius_; }
private:
    double radius_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    void draw() const override {
        std::cout << "画矩形(" << width_ << "x" << height_ << ")" << std::endl;
    }
    double area() const override { return width_ * height_; }
private:
    double width_, height_;
};

// 工厂函数
std::unique_ptr<Shape> createShape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;
}

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(createShape("circle", 5.0));
    shapes.push_back(createShape("rectangle", 4.0, 3.0));
    shapes.push_back(createShape("circle", 2.5));

    double totalArea = 0;
    for (const auto& shape : shapes) {
        shape->draw();
        totalArea += shape->area();
    }

    std::cout << "总面积: " << totalArea << std::endl;
    return 0;
}

工厂函数只关心创建逻辑,完全不需要手动管理内存。调用者拿到 unique_ptr 后,所有权转移清晰明确。

3.3 容器中存储智能指针

当你需要在一个容器里存储多态对象时,make_unique + unique_ptr 是标准做法:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

class Task {
public:
    Task(std::string name, int priority)
        : name_(std::move(name)), priority_(priority) {}
    virtual ~Task() = default;

    virtual void execute() const {
        std::cout << "[" << priority_ << "] " << name_ << std::endl;
    }

    int priority() const { return priority_; }

private:
    std::string name_;
    int priority_;
};

class UrgentTask : public Task {
public:
    using Task::Task;
    void execute() const override {
        std::cout << "⚠️  紧急! ";
        Task::execute();
    }
};

int main() {
    std::vector<std::unique_ptr<Task>> taskQueue;

    taskQueue.push_back(std::make_unique<Task>("写周报", 3));
    taskQueue.push_back(std::make_unique<UrgentTask>("修复线上Bug", 1));
    taskQueue.push_back(std::make_unique<Task>("代码审查", 2));

    // 按优先级排序(升序,数字越小越紧急)
    std::sort(taskQueue.begin(), taskQueue.end(),
        [](const auto& a, const auto& b) {
            return a->priority() < b->priority();
        });

    std::cout << "=== 任务队列 ===" << std::endl;
    for (const auto& task : taskQueue) {
        task->execute();
    }

    return 0;
}

注意 std::make_unique<UrgentTask>("修复线上Bug", 1) 直接构造了派生类对象,但返回的是 unique_ptr<Task>——所有权和多态性完美结合。


四、进阶用法

4.1 数组版本:make_unique<T[]>

make_unique 还支持创建动态数组:

#include <iostream>
#include <memory>

int main() {
    // 创建一个包含 10 个 int 的数组,值初始化为 0
    auto arr = std::make_unique<int[]>(10);

    // 使用下标访问
    for (int i = 0; i < 10; ++i) {
        arr[i] = i * i;
    }

    for (int i = 0; i < 10; ++i) {
        std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
    }

    return 0;
}

注意数组版本的语法:std::make_unique<int[]>(10),模板参数是 int[] 而非 int

⚠️ make_unique<T[]>值初始化数组元素(int 初始化为 0),而 new T[10] 不会(元素值未定义)。这是一个重要的区别。

4.2 make_unique 与 make_shared 的区别

make_uniquemake_shared 虽然都是工厂函数,但有一个关键区别:

特性 make_unique<T> make_shared<T>
控制块分配 无控制块 对象 + 控制块一次分配
引用计数
自定义删除器 ❌ 不支持 ❌ 不支持
内存分配次数 1 次 1 次(合并优化)

make_shared 的一大优势是内存合并——对象和引用计数控制块在同一次 new 中分配,减少了一次内存分配和一次释放,同时对 CPU 缓存更友好。

make_unique 因为 unique_ptr 本身没有控制块,所以没有这个优化需求。它的优势纯粹在于安全性和简洁性


五、注意事项

5.1 不能用于自定义删除器

make_unique 不支持自定义删除器。如果你需要自定义释放逻辑,只能手动构造:

// ❌ make_unique 不支持自定义删除器
// auto fd = std::make_unique<FILE, decltype(&fclose)>(fopen("test.txt", "r"), &fclose);

// ✅ 只能手动构造
auto fileDeleter = [](FILE* fp) { if (fp) fclose(fp); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("test.txt", "r"), fileDeleter);

这是 make_unique 唯一的"硬伤"。不过在实际项目中,需要自定义删除器的场景相对较少(资源句柄通常有现成的 RAII 封装),所以影响不大。

5.2 不能用于 std::make_shared 的场景

如果你需要 weak_ptr 共享同一个对象,或者想利用内存合并优化,应该用 make_shared 而非 make_unique

// 需要 weak_ptr?用 make_shared
auto shared = std::make_shared<Widget>();
std::weak_ptr<Widget> weak = shared;

// 只需要独占?用 make_unique
auto unique = std::make_unique<Widget>();
// std::weak_ptr<Widget> weak = unique;  // ❌ unique_ptr 没有 weak_ptr

5.3 何时应该使用 unique_ptr 而非 shared_ptr

一个简单的判断标准:

  • 所有权明确、单一:用 unique_ptr(轻量、零开销)
  • 所有权需要共享:用 shared_ptr(有控制块开销)

unique_ptr 的独特优势是它可以零开销地转换为 shared_ptr

std::unique_ptr<Widget> uw = std::make_unique<Widget>();
// 需要共享时,无缝升级
std::shared_ptr<Widget> sw = std::move(uw);
// uw 现在为空,sw 接管了所有权

但反向转换——从 shared_ptr 降级为 unique_ptr——是不可能的。所以在设计 API 时,优先返回 unique_ptr,让调用者自己决定是否需要共享。


六、性能对比

make_unique 本身没有任何运行时开销。编译器会对它进行内联优化,最终生成的机器码与手动 new 完全相同。

验证一下:

#include <iostream>
#include <memory>
#include <chrono>

struct HeavyObject {
    int data[1024];
    HeavyObject() { data[0] = 42; }
};

int main() {
    constexpr int N = 1000000;

    // 测试 make_unique
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        auto p = std::make_unique<HeavyObject>();
        (void)p;
    }
    auto end1 = std::chrono::high_resolution_clock::now();

    // 测试手动 new
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        std::unique_ptr<HeavyObject> p(new HeavyObject());
        (void)p;
    }
    auto end2 = std::chrono::high_resolution_clock::now();

    auto ms1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
    auto ms2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();

    std::cout << "make_unique: " << ms1 << " ms" << std::endl;
    std::cout << "手动 new:    " << ms2 << " ms" << std::endl;

    return 0;
}

在我的测试环境中(GCC 13, -O2),两者耗时基本一致,差异在测量误差范围内。

结论:make_unique 是零开销抽象——安全性和简洁性不要你付出任何性能代价。


七、编译器支持

std::make_unique 是 C++14 标准库的一部分,需要 C++14 或更高版本。

编译器 最低版本
GCC 4.9+(2014 年 4 月)
Clang 3.5+(2014 年 9 月)
MSVC VS 2017 15.5+(2017 年 12 月)

编译时需要启用 C++14 标志:

g++ -std=c++14 main.cpp -o main
clang++ -std=c++14 main.cpp -o main
cl /std:c++14 main.cpp

MSVC 在 C++11 模式下也可能提供 make_unique(作为早期实现),但 GCC 和 Clang 的标准库严格要求 C++14 模式。为可移植性考虑,建议显式使用 -std=c++14


总结

std::make_unique 虽然只是 C++14 标准库中"一个小函数",但它解决的问题却很实在:

  • 异常安全:消除了手动 new 带来的裸指针泄漏风险
  • 代码简洁:类型只写一次,无需重复声明
  • 零运行时开销:编译器内联后与手动 new 生成的机器码完全相同
  • 工厂模式的最佳搭档:返回 unique_ptr 是 C++ 中管理多态对象的标准做法

使用建议很简单:在 C++14 及以上,永远用 std::make_unique<T>(args...) 替代 std::unique_ptr<T>(new T(args...))。唯一的例外是需要自定义删除器的场景。

这个小函数体现了 C++ 标准委员会的一个理念:好的工具应该让安全的写法比不安全的写法更简单。当你发现"正确的做法"反而更省事时,你自然就不会犯错了。


📌 下一篇预告: C++20 的重磅特性之一——Modules(模块)。告别 #include 头文件的编译时间噩梦,用模块化的方式组织你的 C++ 代码。敬请期待!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐