Oh, this std :: make_shared ...

The C ++ Core Guidelines contain a R22 rule that says to use std :: make_shared instead of calling the std :: shared_ptr constructor. There is only one argument in Core Guidelines for such a decision - saving on allocation (and deallocation).



And if you dig a little deeper?



std :: make_shared useful



Why did std :: make_shared appear in STL at all?



There is a canonical example where constructing a std :: shared_ptr from a freshly created raw pointer can lead to a memory leak:



process(std::shared_ptr<Bar>(new Bar), foo());


To calculate the arguments of the process (...) function, you must call:



  • new Bar;
  • constructor std :: shared_ptr;
  • foo ().


The compiler can mix them in random order, for example like this:



  • new Bar;
  • foo ();
  • constructor std :: shared_ptr.


If an exception occurs in foo (), we get a leak of the Bar instance.



None of the following code examples contain a potential leak (but we will come back to this question):



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));


I repeat: in order for a potential leak to occur, it is necessary to write such code as in the very first example - one function takes at least two parameters, one of which is initialized by the freshly created nameless std :: shared_ptr, and the second parameter is initialized by calling another function, which may throw exceptions.



And for a potential memory leak to materialize, two more conditions are required:



  • so that the compiler shuffles calls in an unfavorable way;
  • so that the function evaluating the second parameter actually throws an exception.


Such dangerous code is unlikely to occur more often than once in a hundred uses of std :: shared_ptr.

And to compensate for this danger, std :: shared_ptr was supported by a crutch called std :: make_shared.



To slightly sweeten the pill, the following phrase was added to the description of std :: make_shared in the Standard:

Remarks: Implementations should perform no more than one memory allocation.


Note: implementations should produce no more than one memory allocation.



No, this is not a guarantee.

But cppreference says that all known implementations do just that.



This solution aims to improve performance compared to creating std :: shared_ptr by calling a constructor that requires at least two allocations: one to place the object, and the second to control block.



std :: make_shared useless



Starting with c ++ 17, a memory leak in that tricky rare example for which std :: make_shared was added to the STL is no longer possible.



Study Links:





There are several other cases in which std :: make_shared is useless:



std :: make_shared cannot call private constructor
#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 doesn't support custom deleters
… variadic template. , , deleter.

std::make_shared_with_custom_deleter…



It's good to at least learn about these problems at compile time ...



std :: make_shared is harmful



We pass in runtime.



the overloaded operator new and operator delete will be ignored by 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:





And now - the most important thing, for which the article itself was conceived.



Surprisingly, the fact: how std :: shared_ptr will handle the memory can significantly depend on how it was created - using std :: make_shared or using the constructor!



Why is this happening?



Because the "useful" uniform allocation produced by std :: make_shared has an inherent side effect of unnecessary communication between the control block and the managed object. They simply cannot be freed individually. A control block must live as long as there is at least one weak link.



A std :: shared_ptr created using a constructor should expect the following behavior:



  • allocation of a managed object (before calling the constructor, i.e. on the user's side);
  • allocation of the control unit;
  • upon destruction of the last strong reference - calling the destructor of the managed object and freeing the memory it occupies ; if there is not a single weak link, then release the control unit;
  • on destruction of the last weak link in the absence of strong links - release of the control block.


And if created using std :: make_shared:



  • allocation of the managed object and control unit;
  • upon destruction of the last strong link, a call to the destructor of the managed object without freeing the memory occupied by it ; if at the same time there is not a single weak link - the release of the control unit and the memory of the managed object;
  • — .


Creating std :: shared_ptr with std :: make_shared provokes a space leak.



It is impossible to distinguish at runtime exactly how the std :: shared_ptr instance was created.



Let's move on to testing this behavior.



There is a very simple way - use std :: allocate_shared with a custom allocator, which will report all calls to it. But it’s incorrect to distribute the results obtained in this way to std :: make_shared.



A more correct way is to control the total memory consumption. But there is no talk of any cross-platform.



Code for Linux, tested on Ubuntu 20.04 desktop x64. Who is interested in repeating this for other platforms - see here (my experiments with macOs have shown that the TASK_BASIC_INFO option does not track memory release, and TASK_VM_INFO_PURGEABLE is a better candidate).



Monitoring.h
#pragma once

#include <cstdint>

uint64_t memUsage();




Monitoring.cpp
#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;
}




Output to the console when using the std :: shared_ptr constructor:

initial: 5884

before reset: 71424

~ Big

after strong reset: 5884

after weak reset: 5884



Console output when using std :: make_shared:

initial: 5888

before reset: 71428

~ Big

after strong reset: 71428

after weak reset: 5888



Bonus



Still, is it possible to leak memory as a result of code execution



auto bar = std::shared_ptr<Bar>(new Bar);


?

What happens if the allocation of Bar completes successfully, but there is no longer enough memory for the control block?



And what happens if the constructor with custom deleter was called?



The [util.smartptr.shared.const] section of the Standard ensures that when an exception occurs, inside the constructor of std :: shared_ptr:



  • for a constructor without custom deleter, the passed pointer will be deleted using delete or delete [];
  • for a constructor with a custom deleter, the passed pointer will be deleted using this same deleter.


No leakage guaranteed by the Standard.



As a result of a quick reading of the implementations in three compilers (Apple clang version 11.0.3, GCC 9.3.0, MSVC 2019 16.6.2), I can confirm that everything is so.



Output



In C ++ 11 and C ++ 14, the harm from using std :: make_shared could be balanced out by its single useful function.



Since c ++ 17, arithmetic is not at all in favor of std :: make_shared.



The situation is similar with std :: allocate_shared.



Much of the above is true for std :: make_unique as well, but it does less harm.



All Articles