Effective Modern C++ 条款21:优先选用std::make_unique和std::make_shared,而非直接new
摘要:本文探讨了现代C++中智能指针创建的最佳实践,对比了直接使用new与std::make_unique/std::make_shared的优劣。直接使用new存在代码冗余、异常安全性和性能问题,而make系列函数能有效解决这些问题,提供更简洁、安全和高效的实现。但文中也指出其局限性,如不支持自定义析构器、初始化列表等问题,建议根据具体场景灵活选择。最终结论强调make函数是智能指针创建的首选方
在现代C++编程中,智能指针(如std::unique_ptr和std::shared_ptr)已经成为资源管理的最佳实践。然而,如何高效、安全地创建这些智能指针,仍然是一个值得深入探讨的问题。《Effective Modern C++》的条款二十一明确指出,应当优先使用std::make_unique和std::make_shared,而非直接使用new关键字。本文将详细解读这一建议背后的原因,并分析其适用场景与局限性。
一、为什么避免直接使用new?
在C++中,直接使用new关键字来动态分配内存,虽然功能强大,但也伴随着一些潜在的问题。以下是直接使用new的几个主要缺点:
1. 代码冗余
当我们直接使用new时,通常需要显式地指定类型两次:一次用于智能指针的声明,另一次用于new操作符。例如:
std::unique_ptr<Widget> upw2(new Widget);
这种重复的代码不仅增加了编写成本,还容易在类型变更时引发错误。
2. 异常安全性问题
如果在将new返回的指针赋值给智能指针之前发生异常,那么new分配的内存将无法被正确释放,导致内存泄漏。例如:
process(std::shared_ptr<Widget>(new Widget), badFun());
在这段代码中,如果badFun()抛出异常,那么new Widget分配的内存将永远无法被释放。
3. 性能问题
对于std::shared_ptr,直接使用new会导致两次内存分配:一次用于对象本身,另一次用于控制块(control block)。这会带来额外的开销。
二、std::make_unique和std::make_shared的优势
为了解决上述问题,C++标准库提供了两个便捷的函数:std::make_unique(C++14引入)和std::make_shared(C++11引入)。它们不仅简化了代码,还提升了程序的安全性和性能。
1. 代码简洁性
使用std::make_unique和std::make_shared可以显著减少代码量,并避免类型冗余。例如:
// 使用std::make_unique
auto upw = std::make_unique<Widget>();
// 使用std::make_shared
auto spw = std::make_shared<Widget>();
与直接使用new相比,这些代码不仅更简洁,而且更易于维护。
2. 异常安全性
std::make_unique和std::make_shared将内存分配和智能指针的创建作为一个整体操作来处理。如果在内存分配过程中发生异常,函数将直接抛出异常,而不会留下未释放的内存。例如:
std::shared_ptr<Widget> spw = std::make_shared<Widget>();
即使在内存分配过程中发生异常,也不会有任何内存泄漏的风险。
3. 性能优化
对于std::make_shared,它通过一次性分配内存(包括对象和控制块),避免了两次内存分配的开销。这在处理大量共享指针时,可以显著提升性能。例如:
// 使用new会导致两次内存分配
std::shared_ptr<Widget> spw(new Widget);
// 使用make_shared只会分配一次内存
auto spw = std::make_shared<Widget>();
三、std::make_unique和std::make_shared的局限性
尽管std::make_unique和std::make_shared在大多数情况下表现优异,但它们也有一些无法完成的任务。以下是它们的主要局限性:
1. 不支持自定义析构器
std::make_unique和std::make_shared无法接受自定义的析构器(deleter)。如果你需要为智能指针指定特定的内存释放逻辑,那么直接使用new仍然是唯一的选择。
2. 无法直接接受初始化列表
std::make_unique和std::make_shared无法直接接受大括号初始化(std::initializer_list)。例如:
// 直接使用初始化列表会导致编译错误
auto spv = std::make_shared<std::vector<int>>{10, 20};
不过,可以通过间接方式实现:
auto p = {10, 20};
auto spv = std::make_shared<std::vector<int>>(p);
3. 与自定义内存管理的类不兼容
对于那些自定义了operator new或operator delete的类,std::make_shared可能无法正常工作。这是因为std::shared_ptr除了管理对象内存外,还需要管理控制块内存。如果控制块的内存管理方式与对象内存不一致,可能会导致不可预知的问题。
4. 内存释放的延迟问题
对于std::make_shared,由于对象内存和控制块内存是一次性分配的,因此即使std::shared_ptr的引用计数为0,只要还有std::weak_ptr引用该对象,内存就不会被释放。这在某些内存敏感的场景下可能会带来性能问题。
四、适用场景总结
尽管std::make_unique和std::make_shared存在一些局限性,但在大多数情况下,它们仍然是创建智能指针的最佳选择。以下是一些适用场景的总结:
-
优先选择
std::make_unique和std::make_shared的场景:- 当需要创建
std::unique_ptr或std::shared_ptr时。 - 当需要避免类型冗余和内存泄漏风险时。
- 当希望提升代码的可读性和安全性时。
- 当需要创建
-
避免使用
std::make_unique和std::make_shared的场景:- 当需要使用自定义析构器时。
- 当需要直接传递大括号初始化列表时。
- 当处理自定义内存管理的类时。
- 当内存释放的延迟可能带来性能问题时。
五、结论
std::make_unique和std::make_shared是现代C++编程中的重要工具,它们通过减少代码冗余、提升异常安全性以及优化性能,显著简化了智能指针的创建过程。然而,正如任何工具一样,它们也有自己的局限性。在实际编程中,我们需要根据具体场景灵活选择,以充分发挥它们的优势,同时规避潜在的风险。
更多推荐



所有评论(0)