哦,那个std :: make_shared ...

C ++核心准则包含R22规则,该规则说使用std :: make_shared而不是调用std :: shared_ptr构造函数。核心准则中只有这样一种论点,即这样的决定:节省分配(和取消分配)。



如果您进行更深入的研究?



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的内存泄漏不再可能。



研究链接:





还有其他几种情况,其中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有害



让我们进入运行时。



重载的运算符new和operator delete将被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也是正确的,但危害较小。



All Articles