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.


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.


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.


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

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.


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


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.

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.


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 ...

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.