如果您进行更深入的研究?
std :: make_shared有用
为什么std :: make_shared完全出现在STL中?
有一个规范的示例,其中从新创建的原始指针构造std :: shared_ptr可能导致内存泄漏:
process(std::shared_ptr<Bar>(new Bar), foo());
要计算process(...)函数的参数,必须调用:
- 新酒吧;
- 构造函数std :: shared_ptr;
- foo()。
编译器可以按随机顺序混合它们,例如:
- 新酒吧;
- foo();
- 构造函数std :: shared_ptr。
如果在foo()中引发异常,则会泄漏Bar实例。
以下代码示例均未包含潜在的泄漏(但稍后我们将再次讨论该问题):
auto bar = std::shared_ptr<Bar>(new Bar);
auto bar = std::shared_ptr<Bar>(new Bar);
process(bar, foo());
process(std::shared_ptr<Bar>(new Bar));
我重复一遍:对于潜在的泄漏,您需要编写与第一个示例完全相同的代码-一个函数至少需要两个参数,其中一个参数是使用新创建的未命名std :: shared_ptr初始化的,而第二个参数是通过调用另一个可以引发异常的函数来初始化的。
并且为了实现潜在的内存泄漏,还需要另外两个条件:
- 编译器以不利的方式混合调用;
- 因此评估第二个参数的函数实际上会引发异常。
在std :: shared_ptr的一百次使用中,这样的危险代码不太可能发生多次。
为了弥补这种危险,std :: shared_ptr得到了名为std :: make_shared的拐杖的支持。
为了使药丸更甜一点,已在标准中的std :: make_shared描述中添加了以下短语:
备注:实现应执行不超过一个的内存分配。
注意:实现只能产生一个以上的内存分配。
不,这不是保证。
但是cppreference表示,所有已知的实现方式都可以做到这一点。
与创建std :: shared_ptr相比,此解决方案旨在通过调用至少需要两种分配的构造函数来提高性能:一种分配对象,第二种分配控制块。
std :: make_shared没有用
从c ++ 17开始,在这个棘手的罕见示例中,向stl :: make_shared添加到STL的内存泄漏不再可能。
研究链接:
- cppreference.com上的文档 -搜索“直到C ++ 17”;
- PVS-Studio中兔子洞的深度或C ++中的采访
- 有关cppreference.com的更多文档 -项目15。
还有其他几种情况,其中std :: make_shared无效:
std :: make_shared将无法调用私有构造函数
#include <memory>
class Bar
{
public:
static std::shared_ptr<Bar> create()
{
// return std::make_shared<Bar>(); - no build
return std::shared_ptr<Bar>(new Bar);
}
private:
Bar() = default;
};
int main()
{
auto bar = Bar::create();
return 0;
}
std :: make_shared不支持自定义删除器
… variadic template. , , deleter.
std::make_shared_with_custom_deleter…
std::make_shared_with_custom_deleter…
至少在编译时了解这些问题是一件好事...
std :: make_shared有害
让我们进入运行时。
重载的运算符new和operator delete将被std :: make_shared忽略
std::shared_ptr:
std::make_shared:
#include <memory>
#include <iostream>
class Bar
{
public:
void* operator new(size_t)
{
std::cout << __func__ << std::endl;
return ::new Bar();
}
void operator delete(void* bar)
{
std::cout << __func__ << std::endl;
::delete static_cast<Bar*>(bar);
}
};
int main()
{
auto bar = std::shared_ptr<Bar>(new Bar);
// auto bar = std::make_shared<Bar>();
return 0;
}
std::shared_ptr:
operator new
operator delete
std::make_shared:
而现在-本文本身就是最重要的内容。
出乎意料的是,但确实如此:std :: shared_ptr处理内存的方式可能很大程度上取决于其创建方式-使用std :: make_shared还是使用构造函数!
为什么会这样呢?
因为std :: make_shared产生的“有用”统一分配具有控制块和被管理对象之间不必要的通信的固有副作用。它们根本无法单独释放。只要有至少一个弱链接,控制块就必须存在。
使用构造函数创建的std :: shared_ptr应该具有以下行为:
- 分配管理对象(在调用构造函数之前,即在用户端);
- 控制单元的分配;
- 在最后一个强参考销毁之后-调用被管理对象的析构函数并释放它占用的内存 ; 如果没有单个弱链接,则释放控制单元;
- 在没有强链接的情况下破坏最后一个弱链接-释放控制块。
如果使用std :: make_shared创建:
- 分配管理对象和控制单元;
- 在最后一个强参考销毁时-调用被管理对象的析构函数而不释放它占用的内存 ; 如果同时没有单个弱链接-控制单元的释放和被管理对象的内存;
- — .
使用std :: make_shared创建std :: shared_ptr会引起空间泄漏。
在运行时无法准确区分std :: shared_ptr实例是如何创建的。
让我们继续测试此行为。
有一个非常简单的方法-将std :: allocate_shared与自定义分配器一起使用,它将向其报告所有调用。但是将以这种方式获得的结果扩展到std :: make_shared是不正确的。
一种更正确的方法是控制总内存消耗。但是,没有任何跨平台的说法。
给出了在Ubuntu 20.04桌面x64上测试的Linux代码。谁有兴趣在其他平台上重复此操作-请参见此处 (我对macO的实验表明,TASK_BASIC_INFO选项不允许跟踪空闲内存,而TASK_VM_INFO_PURGEABLE是更合适的候选对象)。
监测h
#pragma once
#include <cstdint>
uint64_t memUsage();
监控文件
#include "Monitoring.h"
#include <fstream>
#include <string>
uint64_t memUsage()
{
auto file = std::ifstream("/proc/self/status", std::ios_base::in);
auto line = std::string();
while(std::getline(file, line)) {
if (line.find("VmSize") != std::string::npos) {
std::string toConvert;
for (const auto& elem : line) {
if (std::isdigit(elem)) {
toConvert += elem;
}
}
return stoull(toConvert);
}
}
return 0;
}
main.cpp
#include <iostream>
#include <array>
#include <numeric>
#include <memory>
#include "Monitoring.h"
struct Big
{
~Big()
{
std::cout << __func__ << std::endl;
}
std::array<volatile unsigned char, 64*1024*1024> _data;
};
volatile uint64_t accumulator = 0;
int main()
{
std::cout << "initial: " << memUsage() << std::endl;
auto strong = std::shared_ptr<Big>(new Big);
// auto strong = std::make_shared<Big>();
std::accumulate(strong->_data.cbegin(), strong->_data.cend(), accumulator);
auto weak = std::weak_ptr<Big>(strong);
std::cout << "before reset: " << memUsage() << std::endl;
strong.reset();
std::cout << "after strong reset: " << memUsage() << std::endl;
weak.reset();
std::cout << "after weak reset: " << memUsage() << std::endl;
return 0;
}
使用std :: shared_ptr构造函数时的控制台输出:
初始值:5884
复位前:71424〜 强复位 后
大
:
弱复位后5884:5884
使用std :: make_shared时的控制台输出:
初始值:5888
,复位前:71428〜 强复位 后,
大
:71428
,弱复位后:5888
奖金
但是,由于执行代码而可能会导致内存泄漏
auto bar = std::shared_ptr<Bar>(new Bar);
?
如果Bar分配成功,但是控制块没有足够的内存,会发生什么?
如果使用自定义删除器调用构造函数会怎样?
该标准的[util.smartptr.shared.const]部分可确保std :: shared_ptr构造函数内部发生异常时:
- 对于没有自定义删除器的构造函数,将使用delete或delete []删除传递的指针。
- 对于具有自定义删除器的构造函数,将使用相同的删除器删除传递的指针。
标准不保证泄漏。
由于粗读了三个编译器(Apple clang版本11.0.3,GCC 9.3.0,MSVC 2019 16.6.2)中的实现,我可以确认是这种情况。
输出量
在c ++ 11和c ++ 14中,使用std :: make_shared所带来的危害可以通过其唯一有用的功能加以平衡。
从c ++ 17开始,算术根本不支持std :: make_shared。
这种情况与std :: allocate_shared类似。
以上对于std :: make_unique也是正确的,但危害较小。