Que tipo de ponteiro eu uso quando?

228

Ok, então a última vez que escrevi C ++ para viver, std::auto_ptrera tudo o que a lib std tinha disponível, e boost::shared_ptrera toda a raiva. Eu realmente nunca olhei para os outros tipos de ponteiros inteligentes fornecidos. Entendo que o C ++ 11 agora fornece alguns dos tipos que o impulso propiciou, mas nem todos.

Então, alguém tem um algoritmo simples para determinar quando usar qual ponteiro inteligente? De preferência, incluindo conselhos sobre ponteiros mudos (como ponteiros brutos T*) e o restante dos ponteiros inteligentes de impulso. (Algo como isso seria ótimo).

sbi
fonte
Veja também std :: auto_ptr para std :: unique_ptr
Martin York
1
Estou realmente esperando que alguém crie um fluxograma útil como este fluxograma de seleção STL .
usar o seguinte
1
@ Als: Oh, isso é realmente um bom! Eu fiz perguntas frequentes.
SBI
6
@ Deduplicator Isso nem chega a ser uma duplicata. A pergunta vinculada diz "Quando devo usar um ponteiro inteligente" e esta pergunta é "Quando devo usar esses ponteiros inteligentes?" isto é, este está categorizando os diferentes usos dos ponteiros inteligentes padrão. A questão vinculada não faz isso. A diferença é aparentemente pequena, mas é grande.
Rapptz

Respostas:

183

Propriedade compartilhada:
o shared_ptre weak_ptro padrão adotado são praticamente os mesmos de seus colegas do Boost . Use-os quando precisar compartilhar um recurso e não souber qual será o último a estar vivo. Use weak_ptrpara observar o recurso compartilhado sem influenciar sua vida útil, para não interromper os ciclos. Ciclos com shared_ptrnormalmente não deveriam acontecer - dois recursos não podem ser proprietários um do outro.

Observe que o Boost oferece adicionalmente shared_array, o que pode ser uma alternativa adequada shared_ptr<std::vector<T> const>.

Em seguida, o Boost oferece intrusive_ptr, que é uma solução leve, se o seu recurso já oferece gerenciamento com contagem de referência e você deseja adotá-lo no princípio RAII. Este não foi adotado pela norma.

Propriedade exclusiva: o
Boost também possui um scoped_ptr, que não é copiável e para o qual você não pode especificar um deleter. std::unique_ptrestá boost::scoped_ptrem esteróides e deve ser sua escolha padrão quando você precisa de um ponteiro inteligente . Ele permite que você especifique um deleter em seus argumentos de modelo e é móvel , ao contrário boost::scoped_ptr. Também é totalmente utilizável em contêineres STL, desde que você não use operações que precisem de tipos copiáveis ​​(obviamente).

Observe novamente que o Boost possui uma versão de matriz:, scoped_arrayque é o padrão unificado por exigir std::unique_ptr<T[]>especialização parcial que fará com que delete[]o ponteiro seja substituído deletepor ele (com default_deleter). std::unique_ptr<T[]>também oferece em operator[]vez de operator*e operator->.

Observe que std::auto_ptrainda está no padrão, mas está obsoleto . §D.10 [depr.auto.ptr]

O modelo de classe auto_ptrestá obsoleto. [ Nota: O modelo de classe unique_ptr(20.7.1) fornece uma solução melhor. - end note ]

Sem propriedade:
use ponteiros mudos (ponteiros brutos) ou referências para referências não proprietárias de recursos e quando souber que o recurso sobreviverá ao objeto / escopo de referência. Prefira referências e use ponteiros brutos quando precisar de nulidade ou redefinição.

Se você deseja uma referência não proprietária a um recurso, mas não sabe se o recurso sobreviverá ao objeto que o referencia, coloque o recurso em um shared_ptre use a weak_ptr- você pode testar se o pai shared_ptrestá vivo lock, o que retorne um shared_ptrque não seja nulo se o recurso ainda existir. Se quiser testar se o recurso está inoperante, use expired. Os dois podem parecer semelhantes, mas são muito diferentes diante da execução simultânea, pois expiredapenas garantem seu valor de retorno para essa única instrução. Um teste aparentemente inocente como

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

é uma condição potencial de corrida.

Xeo
fonte
1
No caso de nenhuma propriedade, você provavelmente deve preferir referências a ponteiros, a menos que não precise de propriedade e reconfiguração, onde as referências não a afetam, mesmo assim, você pode querer reescrever o objeto original como um shared_ptre o ponteiro que não possui. um weak_ptr...
David Rodríguez - dribeas
2
Não quis dizer referência ao ponteiro , mas referência ao invés de ponteiro. Se não houver propriedade, a menos que você precise de redefinição (ou nulidade, mas a nulidade sem poder redefinir seria bastante limitada), você pode usar uma referência simples em vez de um ponteiro.
David Rodríguez - dribeas
1
@ David: Ah, entendo. :) Sim, as referências não são ruins para isso, eu pessoalmente prefiro-as também nesses casos. Vou adicioná-los.
Xeo
1
@ Xeo: shared_array<T>é uma alternativa para shared_ptr<T[]>não shared_ptr<vector<T>>: não pode crescer.
R. Martinho Fernandes
1
@GregroyCurrie: Foi ... exatamente o que eu escrevi? Eu disse que é um exemplo de uma condição potencial de corrida.
Xeo
127

Decidir qual ponteiro inteligente usar é uma questão de propriedade . Quando se trata de gerenciamento de recursos, o objeto A possui o objeto B se estiver no controle da vida útil do objeto B. Por exemplo, as variáveis ​​de membro pertencem aos seus respectivos objetos porque o tempo de vida das variáveis ​​de membro está vinculado à vida útil do objeto. Você escolhe ponteiros inteligentes com base em como o objeto pertence.

Observe que a propriedade de um sistema de software é separada da propriedade, como poderíamos pensar fora do software. Por exemplo, uma pessoa pode "possuir" sua casa, mas isso não significa necessariamente que um Personobjeto tenha controle sobre a vida útil de um Houseobjeto. Confundir esses conceitos do mundo real com os conceitos de software é uma maneira infalível de se programar em um buraco.


Se você possui a propriedade exclusiva do objeto, use std::unique_ptr<T>.

Se você compartilhou a propriedade do objeto ...
- Se não houver ciclos na propriedade, use std::shared_ptr<T>.
- Se houver ciclos, defina uma "direção" e use std::shared_ptr<T>em uma direção e std::weak_ptr<T>na outra.

Se o objeto é seu, mas existe o potencial de não ter dono, use ponteiros normais T*(por exemplo, ponteiros pai).

Se o objeto lhe pertence (ou tem existência garantida), use referências T&.


Advertência: Esteja ciente dos custos de indicadores inteligentes. Em ambientes com desempenho limitado ou de memória, pode ser benéfico usar ponteiros normais com um esquema mais manual para gerenciar a memória.

Os custos:

  • Se você possui um deleter personalizado (por exemplo, você usa pools de alocação), isso gera uma sobrecarga por ponteiro que pode ser facilmente evitada pela exclusão manual.
  • std::shared_ptrpossui a sobrecarga de um incremento da contagem de referência na cópia, além de um decréscimo na destruição seguido de uma verificação de contagem de 0 com exclusão do objeto em espera. Dependendo da implementação, isso pode inchar seu código e causar problemas de desempenho.
  • Tempo de compilação. Como em todos os modelos, ponteiros inteligentes contribuem negativamente para os tempos de compilação.

Exemplos:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Uma árvore binária não possui seu pai, mas a existência de uma árvore implica a existência de seu pai (ou nullptrraiz), de modo que usa um ponteiro normal. Uma árvore binária (com semântica de valores) possui propriedade exclusiva de seus filhos, de modo que são std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Aqui, o nó da lista possui suas listas seguintes e anteriores, portanto, definimos uma direção e usamos shared_ptrpara next e weak_ptrfor prev para interromper o ciclo.

Peter Alexander
fonte
3
No exemplo da árvore binária, algumas pessoas sugerem o uso shared_ptr<BinaryTree>para os filhos e weak_ptr<BinaryTree>para o relacionamento dos pais.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas: Depende se a Árvore tem semântica de valor ou não. Se as pessoas fizerem referência à sua árvore externamente, mesmo depois que a árvore de origem for destruída, sim, a combinação de ponteiro compartilhado / fraco seria melhor.
Peter Alexander
Se um objeto é seu e é garantido que ele existe, por que não uma referência?
Martin York
1
Se você usar referência, nunca poderá alterar o pai, o que pode ou não prejudicar o design. Para equilibrar árvores, isso dificultaria.
Mooing Duck
3
+1, mas você deve adicionar uma definição de "propriedade" na primeira linha. Muitas vezes me vejo tendo que declarar claramente que é sobre a vida e a morte do objeto, e não sobre um significado mais específico do domínio.
Klaim
19

Use unique_ptr<T>o tempo todo, exceto quando você precisar da contagem de referência, nesse caso, use shared_ptr<T>(e para casos muito raros, weak_ptr<T>para evitar ciclos de referência). Em quase todos os casos, a propriedade exclusiva transferível é ótima.

Ponteiros brutos: bom apenas se você precisar de retornos covariantes, apontamentos não proprietários, o que pode acontecer. Caso contrário, não são terrivelmente úteis.

Ponteiros de matriz: unique_ptrpossui uma especialização para a T[]qual chama automaticamente delete[]o resultado, para que você possa fazê-lo com segurança, unique_ptr<int[]> p(new int[42]);por exemplo. shared_ptrvocê ainda precisaria de um deleter personalizado, mas não precisaria de um ponteiro de matriz exclusivo ou compartilhado especializado. Naturalmente, essas coisas geralmente são melhor substituídas de std::vectorqualquer maneira. Infelizmente shared_ptr, não fornece uma função de acesso à matriz; portanto, você ainda precisa chamar manualmente get(), mas unique_ptr<T[]>fornece em operator[]vez de operator*e operator->. Em qualquer caso, você deve verificar seus limites. Isso torna shared_ptrum pouco menos fácil de usar, embora indiscutivelmente a vantagem genérica e nenhuma dependência do Boost faça unique_ptre shared_ptros vencedores novamente.

Indicadores de escopo: tornados irrelevantes por unique_ptr, exatamente como auto_ptr.

Não há realmente mais nada a ver. No C ++ 03 sem semântica de movimentação, essa situação era muito complicada, mas no C ++ 11 o conselho é muito simples.

Ainda existem usos para outros ponteiros inteligentes, como intrusive_ptrou interprocess_ptr. No entanto, eles são muito nicho e completamente desnecessários no caso geral.

Cachorro
fonte
Além disso, ponteiros brutos para iteração. E para buffers de parâmetros de saída, em que o buffer pertence ao chamador.
Ben Voigt
Hmm, do jeito que eu li isso, são situações que retornam covariantes e não possuem. Uma reescrita pode ser boa se você quis dizer a união e não a interseção. Eu diria também que a iteração também merece menção especial.
Ben Voigt
2
std::unique_ptr<T[]>fornece em operator[]vez de operator*e operator->. É verdade que você ainda precisa fazer uma verificação obrigatória.
Xeo
8

Casos de quando usar unique_ptr:

  • Métodos de fábrica
  • Membros que são ponteiros (pimpl incluído)
  • Armazenando ponteiros em contêineres stl (para evitar movimentos)
  • Uso de grandes objetos dinâmicos locais

Casos de quando usar shared_ptr:

  • Compartilhando objetos entre threads
  • Compartilhando objetos em geral

Casos de quando usar weak_ptr:

  • Mapa grande que funciona como referência geral (por exemplo, um mapa de todos os soquetes abertos)

Sinta-se livre para editar e adicionar mais

Lalaland
fonte
Na verdade, gosto mais da sua resposta à medida que você fornece cenários.
Nicholas Humphrey,