Ah, esse std :: make_shared ...

As Diretrizes Principais do C ++ contêm uma regra R22 que diz usar std :: make_shared em vez de chamar o construtor std :: shared_ptr. Há apenas um argumento nas diretrizes principais para tal decisão - economia na alocação (e desalocação).



E se você cavar um pouco mais fundo?



std :: make_shared útil



Por que std :: make_shared apareceu no STL?



Há um exemplo canônico em que a construção de um std :: shared_ptr a partir de um ponteiro bruto recém-criado pode levar a um vazamento de memória:



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


Para calcular os argumentos para a função de processo (...), você deve chamar:



  • novo bar;
  • construtor std :: shared_ptr;
  • foo ().


O compilador pode misturá-los em qualquer ordem, por exemplo:



  • novo bar;
  • foo ();
  • construtor std :: shared_ptr.


Se uma exceção é lançada em foo (), obtemos um vazamento da instância Bar.



Nenhum dos exemplos de código a seguir contém um vazamento em potencial (mas voltaremos a esta pergunta mais tarde):



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


Repito: para que um possível vazamento ocorra, é necessário escrever esse código como no primeiro exemplo - uma função usa pelo menos dois parâmetros, um dos quais é inicializado pelo std :: shared_ptr sem nome recém-criado, e o segundo parâmetro é inicializado chamando outra função, que pode gerar exceções.



E, para que um possível vazamento de memória seja realizado, são necessárias mais duas condições:



  • para que o compilador embaralhe as chamadas de maneira desfavorável;
  • para que a função que avalia o segundo parâmetro realmente lança uma exceção.


É improvável que esse código perigoso seja mais comum do que uma vez a cada cem usos de std :: shared_ptr.

E para compensar esse perigo, o std :: shared_ptr foi suportado por uma muleta chamada std :: make_shared.



Para adoçar um pouco a pílula, a seguinte frase foi adicionada à descrição std :: make_shared no padrão:

Comentários: As implementações devem executar não mais que uma alocação de memória.


Nota: Implementações devem fazer não mais do que uma alocação de memória.



Não, isso não é uma garantia.

Mas a cppreference diz que todas as implementações conhecidas fazem exatamente isso.



Esta solução tem como objetivo melhorar o desempenho em comparação com a criação de um std :: shared_ptr usando uma chamada de construtor, que requer pelo menos duas alocações: uma para colocar o objeto e outra para o bloco de controle.



std :: make_shared é inútil



A partir do c ++ 17, um vazamento de memória nesse exemplo raro e complicado para o qual std :: make_shared foi adicionado ao STL não é mais possível.



Links de estudo:





Existem vários outros casos em que std :: make_shared é inútil:



std :: make_shared não poderá chamar o construtor privado
#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 não suporta deleters personalizados
… variadic template. , , deleter.

std::make_shared_with_custom_deleter…



É bom, pelo menos, aprender sobre esses problemas em tempo de compilação ...



std :: make_shared é prejudicial



Passamos em tempo de execução.



o operador sobrecarregado new e operator delete serão ignorados por 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:





E agora - a coisa mais importante, para a qual o próprio artigo foi concebido.



Surpreendentemente, é verdade: como o std :: shared_ptr manipulará a memória pode depender significativamente de como ela foi criada - usando std :: make_shared ou usando um construtor!



Por que isso está acontecendo?



Como a alocação uniforme "útil" produzida pelo std :: make_shared tem um efeito colateral inerente à comunicação desnecessária entre o bloco de controle e o objeto gerenciado. Eles simplesmente não podem ser libertados individualmente. Um bloco de controle deve permanecer enquanto houver pelo menos um link fraco.



Um std :: shared_ptr criado usando um construtor deve esperar o seguinte comportamento:



  • alocação de um objeto gerenciado (antes de chamar o construtor, ou seja, do lado do usuário);
  • alocação da unidade de controle;
  • após a destruição do último elo forte, a chamada do destruidor do objeto gerenciado e a liberação da memória ocupada por ele ; se ao mesmo tempo não houver um único elo fraco - liberação da unidade de controle;
  • após a destruição do último elo fraco na ausência de elos fortes - a liberação da unidade de controle.


E se criado usando std :: make_shared:



  • alocação do objeto gerenciado e unidade de controle;
  • após a destruição da última referência forte - chamar o destruidor do objeto gerenciado sem liberar a memória que ocupa ; se ao mesmo tempo não houver um único link fraco - libere o bloco de controle e a memória do objeto gerenciado;
  • — .


Criar std :: shared_ptr com std :: make_shared provoca um vazamento de espaço.



É impossível distinguir em tempo de execução exatamente como a instância std :: shared_ptr foi criada.



Passamos a testar esse comportamento.



Existe uma maneira muito simples - use std :: assignate_shared com alocador personalizado, que reportará todas as chamadas para ele. Mas é incorreto estender os resultados obtidos dessa maneira para std :: make_shared.



Uma maneira mais correta é controlar o consumo total de memória. Mas não há dúvida de nenhuma plataforma cruzada aqui.



Código para Linux, testado no Ubuntu 20.04 desktop x64. Quem está interessado em repetir isso para outras plataformas - veja aqui (minhas experiências com macOs mostraram que a opção TASK_BASIC_INFO não rastreia a liberação de memória e TASK_VM_INFO_PURGEABLE é um candidato melhor).



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




Saída para o console ao usar o construtor std :: shared_ptr:

inicial: 5884

antes da redefinição: 71424

~ Grande

após redefinição forte: 5884

após redefinição fraca: 5884



Saída para o console ao usar std :: make_shared:

inicial: 5888

antes da redefinição: 71428

~ Grande

após redefinição forte: 71428

após redefinição fraca: 5888



Bônus



Ainda, é possível vazamento de memória como resultado da execução do código



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


?

O que acontece se a alocação de Bar for concluída com êxito, mas não houver mais memória suficiente para o bloco de controle?



O que acontece se o construtor foi chamado com um deleter personalizado?



A seção [util.smartptr.shared.const] do padrão garante que, quando ocorrer uma exceção, dentro do construtor de std :: shared_ptr:



  • para um construtor sem deleter personalizado, o ponteiro transmitido será excluído usando delete ou delete [];
  • para um construtor com deleter personalizado, o ponteiro transmitido será excluído usando esse próprio deleter.


Nenhum vazamento garantido pelo padrão.



Como resultado de uma leitura superficial das implementações em três compiladores (Apple clang versão 11.0.3, GCC 9.3.0, MSVC 2019 16.6.2), posso confirmar que esse é o caso.



Resultado



Em c ++ 11 e c ++ 14, o dano do uso de std :: make_shared pode ser compensado por sua única função útil.



Desde o c ++ 17, a aritmética não é de todo favorável a std :: make_shared.



A situação é semelhante ao std :: assignate_shared.



Muito do acima exposto também é válido para std :: make_unique, mas há menos danos a ele.



All Articles