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

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

Muitas postagens do google e stackoverflow estão lá, mas não consigo entender por que o make_shared é mais eficiente do que usar diretamente o shared_ptr .

Alguém pode me explicar passo a passo a seqüência de objects criados e as operações feitas por ambos para que eu seja capaz de entender como o make_shared é eficiente. Eu dei um exemplo acima para referência.

    A diferença é que std::make_shared executa uma alocação de heap, enquanto que chamar o construtor std::shared_ptr executa dois.

    Onde as alocações de heap acontecem?

    std::shared_ptr gerencia duas entidades:

    • o bloco de controle (armazena metadados como ref-counts, deleter apagado por tipo, etc)
    • o object sendo gerenciado

    std::make_shared executa 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 heap para os dados gerenciados e o construtor std::shared_ptr executa outro para o bloco de controle.

    Para mais informações, confira as notas de implementação na cppreference .

    Atualização I: exceção de segurança

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

    Considere este exemplo,

     void F(const std::shared_ptr &lhs, const std::shared_ptr &rhs) { /* ... */ } F(std::shared_ptr(new Lhs("foo")), std::shared_ptr(new Rhs("bar"))); 

    Como o C ++ permite uma 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
    4. std::shared_ptr

    Agora, suponha que tenhamos uma exceção lançada na etapa 2 (por exemplo, exceção out of memory, o construtor Rhs emitiu alguma exceção). Em seguida, perdemos a memory alocada na etapa 1, pois nada terá a chance de limpá-la. O núcleo do problema aqui é que o ponteiro bruto não foi passado para o construtor std::shared_ptr imediatamente.

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

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

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

     F(std::make_shared("foo"), std::make_shared("bar")); 

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

    Citando os comentários de Casey :

    Como há apenas uma alocação, a memory do pointee não pode ser desalocada até que o bloco de controle não esteja mais em uso. Um weak_ptr pode manter o bloco de controle ativo indefinidamente.

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

    Deve haver uma maneira de o weak_ptr s determinar se o object gerenciado ainda é válido (por exemplo, para lock ). Eles fazem isso verificando o número de shared_ptr s que possuem o object gerenciado, que é armazenado no bloco de controle. O resultado é que os blocos de controle estão weak_ptr até que a contagem shared_ptr e a contagem weak_ptr atinjam 0.

    Voltar para std::make_shared

    Como std::make_shared faz uma única alocação de heap para o bloco de controle e o object gerenciado, não há como liberar a memory para o bloco de controle e o object gerenciado independentemente. Devemos esperar até podermos liberar o bloco de controle e o object gerenciado, o que acontece até que não haja shared_ptr s ou weak_ptr s vivos.

    Suponha que, em vez disso, tenhamos executado duas alocações de heap para o bloco de controle e o object gerenciado por meio do construtor new e shared_ptr . Então nós liberamos a memory para o object gerenciado (talvez mais cedo) quando não há shared_ptr ativo, e weak_ptr a memory para o bloco de controle (talvez mais tarde) quando não houver weak_ptr ativo.

    O ponteiro compartilhado gerencia o object em si e um object pequeno contendo a contagem de referência e outros dados de manutenção. make_shared pode alocar um único bloco de memory para armazenar os dois; A construção de um ponteiro compartilhado de um ponteiro para um object já alocado precisará alocar um segundo bloco para armazenar a contagem de referência.

    Além dessa eficiência, o uso de make_shared significa que você não precisa lidar com pointers new e crus, dando melhor segurança à exceção – não há possibilidade de lançar uma exceção depois de alocar o object, mas antes de atribuí-lo ao ponteiro inteligente .

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

     class A { public: A(): val(0){} std::shared_ptr createNext(){ return std::make_shared(val+1); } // Invalid because make_shared needs to call A(int) **internally** std::shared_ptr createNext(){ return std::shared_ptr(new A(val+1)); } // Works fine because A(int) is called explicitly private: int val; A(int v): val(v){} }; 

    Se você precisar de alinhamento especial de memory no object controlado por shared_ptr, não poderá confiar no make_shared, mas acho que é o único bom motivo para não usá-lo.

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

    Sobre eficiência e preocupação com o tempo gasto na alocação, fiz este teste simples abaixo, criei muitas instâncias através dessas duas formas (uma de cada vez):

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

    A coisa é, usando make_shared levou o tempo duplo em comparação com o uso de novo. Então, usando new existem duas alocações de heap ao invés de uma usando make_shared. Talvez este seja um teste estúpido, mas não mostra que usar o make_shared leva mais tempo do que usar o novo? Claro, estou falando de tempo usado apenas.

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