Diferença no make_shared e shared_ptr normal em C ++

276
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Muitas postagens do google e do stackoverflow existem sobre isso, mas não consigo entender por que make_sharedé mais eficiente do que usar diretamente shared_ptr.

Alguém pode me explicar passo a passo a sequência de objetos criados e operações realizadas por ambos, para que eu possa entender como make_sharedé eficiente. Eu dei um exemplo acima para referência.

Anup Buchke
fonte
4
Não é mais eficiente. O motivo para usá-lo é para segurança excepcional.
Yuushi 03/03
Alguns artigos dizem que isso evita algumas despesas gerais de construção. Você pode explicar mais sobre isso?
Anup Buchke
16
@Yuushi: A exceção de segurança é um bom motivo para usá-la, mas também é mais eficiente.
Mike Seymour
3
32:15 é onde ele começa no vídeo ao qual vinculei acima, se isso ajudar.
chris
4
Menor vantagem no estilo de código: usando make_sharedvocê pode escrever auto p1(std::make_shared<A>())e p1 terá o tipo correto.
precisa saber é o seguinte

Respostas:

333

A diferença é que std::make_sharedexecuta uma alocação de heap, enquanto que chamar o std::shared_ptrconstrutor executa duas.

Onde as alocações de heap acontecem?

std::shared_ptr gerencia duas entidades:

  • o bloco de controle (armazena metadados como contagens de ref, deletador apagado por tipo etc.)
  • o objeto que está sendo gerenciado

std::make_sharedexecuta uma única contabilidade de alocação de heap para o espaço necessário para o bloco de controle e os dados. No outro caso, new Obj("foo")chama uma alocação de pilha para os dados gerenciados e o std::shared_ptrconstrutor executa outra para o bloco de controle.

Para mais informações, consulte as notas de implementação em cppreference .

Atualização I: Exceção-Segurança

NOTA (30/08/2019) : este não é um problema desde o C ++ 17, devido às alterações na ordem de avaliação dos argumentos da função. Especificamente, é necessário que cada argumento de uma função seja executado completamente antes da avaliação de outros argumentos.

Como o OP parece estar se perguntando sobre o lado da exceção-segurança, atualizei minha resposta.

Considere este exemplo,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

Como o C ++ permite a ordem arbitrária de avaliação de subexpressões, uma ordem possível é:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Agora, suponha que recebemos uma exceção lançada na etapa 2 (por exemplo, exceção de falta de memória, o Rhsconstrutor lançou alguma exceção). Em seguida, perdemos a memória alocada na etapa 1, pois nada terá a chance de limpá-la. O principal do problema aqui é que o ponteiro bruto não foi passado para o std::shared_ptrconstrutor imediatamente.

Uma maneira de corrigir isso é fazê-lo em linhas separadas, para que essa ordenação arbitrária não possa ocorrer.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

A maneira preferida de resolver isso, é claro, é usar std::make_shared.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Atualização II: Desvantagem de std::make_shared

Citando os comentários de Casey :

Como existe apenas uma alocação, a memória do apontador não pode ser desalocada até que o bloco de controle não esteja mais em uso. A weak_ptrpode manter o bloco de controle ativo indefinidamente.

Por que instâncias de weak_ptrs mantêm o bloco de controle ativo?

Deve haver uma maneira de weak_ptrs determinar se o objeto gerenciado ainda é válido (por exemplo, para lock). Eles fazem isso verificando o número de shared_ptrs que possuem o objeto gerenciado, que é armazenado no bloco de controle. O resultado é que os blocos de controle permanecem ativos até a shared_ptrcontagem e a weak_ptrcontagem atingirem 0.

De volta a std::make_shared

Como std::make_sharedfaz uma única alocação de heap para o bloco de controle e o objeto gerenciado, não há como liberar a memória para o bloco de controle e o objeto gerenciado independentemente. Devemos esperar até que possamos liberar o bloco de controle e o objeto gerenciado, o que acontece até que não haja nenhum shared_ptrou mais weak_ptrativos.

Suponha que, em vez disso, realizássemos duas alocações de heap para o bloco de controle e o objeto gerenciado via newe shared_ptrconstrutor. Em seguida, liberamos a memória para o objeto gerenciado (talvez mais cedo) quando não há nenhum shared_ptrativo, e liberamos a memória para o bloco de controle (talvez mais tarde) quando não há nenhum weak_ptrativo.

mpark
fonte
53
É uma boa idéia mencionar também a pequena desvantagem das esquinas make_shared: como existe apenas uma alocação, a memória do apontador não pode ser desalocada até que o bloco de controle não esteja mais em uso. A weak_ptrpode manter o bloco de controle ativo indefinidamente.
Casey
14
Outra, mais estilística, ponto é: Se você usar make_sharede make_uniquede forma consistente, você não vai ter possuir ponteiros crus um pode tratar todas as ocorrências de newcomo um cheiro de código.
Philipp
6
Se houver apenas um shared_ptr, e não weak_ptrs, chamar reset()a shared_ptrinstância excluirá o bloco de controle. Mas isso é independente ou se make_sharedfoi usado. O uso make_sharedfaz a diferença, pois pode prolongar a vida útil da memória alocada para o objeto gerenciado . Quando a shared_ptrcontagem atinge 0, o destruidor do objeto gerenciado é chamado independentemente make_shared, mas liberar sua memória só pode ser feito se não tiver make_sharedsido usado. Espero que isso torne mais claro.
mpark
4
Também vale a pena mencionar que o make_shared pode tirar proveito da otimização "Nós sabemos onde você mora", que permite que o bloco de controle seja um ponteiro menor. (Para obter detalhes, consulte a apresentação GN2012 de Stephan T. Lavavej, por volta do minuto 12.) make_shared, portanto, não apenas evita uma alocação, mas também aloca menos memória total.
KnowItAllWannabe
1
@HannaKhalil: Esse talvez seja o reino do que você está procurando ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

O ponteiro compartilhado gerencia o próprio objeto e um pequeno objeto que contém a contagem de referência e outros dados de manutenção. make_sharedpode alocar um único bloco de memória para armazenar os dois; a construção de um ponteiro compartilhado de um ponteiro para um objeto já alocado precisará alocar um segundo bloco para armazenar a contagem de referência.

Além dessa eficiência, usar make_sharedsignifica que você não precisa lidar com newponteiros brutos, fornecendo melhor segurança para exceções - não há possibilidade de lançar uma exceção após alocar o objeto, mas antes de atribuí-lo ao ponteiro inteligente.

Mike Seymour
fonte
2
Eu entendi seu primeiro ponto corretamente. Você pode elaborar ou fornecer alguns links sobre o segundo ponto sobre segurança de exceção?
Anup Buchke
22

Há outro caso em que as duas possibilidades diferem, além das já mencionadas: se você precisar chamar um construtor não público (protegido ou privado), o make_shared poderá não ser capaz de acessá-lo, enquanto a variante do novo funciona bem .

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};
Dr_Sam
fonte
Eu me deparei com esse problema exato e decidi usar new, caso contrário eu teria usado make_shared. Aqui está uma pergunta relacionada sobre isso: stackoverflow.com/questions/8147027/… .
jigglypuff
6

Se você precisar de um alinhamento especial da memória no objeto controlado por shared_ptr, não poderá confiar no make_shared, mas acho que é a única boa razão para não usá-lo.

Simon Ferquel
fonte
2
Uma segunda situação em que make_shared é inadequado é quando você deseja especificar um deleter personalizado.
KnowItAllWannabe
5

Eu vejo um problema com std :: make_shared, ele não suporta construtores privados / protegidos

icebeat
fonte
3

Shared_ptr: Executa duas alocações de heap

  1. Bloco de controle (contagem de referência)
  2. Objeto sendo gerenciado

Make_shared: Executa apenas uma alocação de heap

  1. Bloco de controle e dados do objeto.
James
fonte
0

Sobre eficiência e preocupação com o tempo gasto na alocação, fiz este teste simples abaixo, criei várias instâncias por essas duas maneiras (uma de cada vez):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

O problema é que o uso do make_shared levou o dobro do tempo comparado ao uso do novo. Portanto, usando new, existem duas alocações de heap em vez de uma usando make_shared. Talvez este seja um teste estúpido, mas não mostra que o uso do make_shared leva mais tempo do que o novo? Claro, estou falando apenas do tempo usado.

orlando
fonte
4
Esse teste é um tanto inútil. O teste foi realizado na configuração do release com otimizações geradas? Além disso, todos os seus itens são liberados imediatamente, para que não sejam realistas.
precisa saber é o seguinte
0

Penso que a parte de exceção da segurança da resposta do sr. Mpark ainda é uma preocupação válida. ao criar um shared_ptr como este: shared_ptr <T> (novo T), o novo T pode ter êxito, enquanto a alocação do bloco de controle do shared_ptr pode falhar. Nesse cenário, o T alocado recentemente vazará, pois o shared_ptr não tem como saber que foi criado no local e é seguro excluí-lo. Ou eu estou esquecendo de alguma coisa? Eu não acho que as regras mais rígidas sobre avaliação de parâmetros de função ajudem de alguma forma aqui ...

Martin Vorbrodt
fonte