raw, fraca_ptr, único_ptr, shared_ptr etc ... Como escolhê-los com sabedoria?

33

Existem muitos ponteiros em C ++, mas para ser sincero em 5 anos ou mais na programação C ++ (especificamente com o Qt Framework), eu só uso o ponteiro bruto antigo:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Eu sei que existem muitos outros ponteiros "inteligentes":

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Mas não tenho a menor idéia do que fazer com eles e o que eles podem me oferecer em comparação com indicadores brutos.

Por exemplo, eu tenho esse cabeçalho de classe:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Claramente, isso não é exaustivo, mas para cada um desses três indicadores é correto deixá-los "em bruto" ou devo usar algo mais apropriado?

E, na segunda vez, se um empregador ler o código, ele será rigoroso quanto ao tipo de indicação que eu uso ou não?

CheshireChild
fonte
Este tópico parece tão apropriado para o SO. isso foi em 2008 . E aqui está: Que tipo de ponteiro devo usar quando? . Tenho certeza que você pode encontrar correspondências ainda melhores. Estas foram apenas as primeiras que eu vi
veja
imo esse limite, pois trata tanto do significado / intenção conceitual dessas classes quanto dos detalhes técnicos de seu comportamento e implementações. Como a resposta aceita se inclina para a primeira, fico feliz em ter essa "versão PSE" dessa pergunta do SO.
Ixrec

Respostas:

70

Um ponteiro "bruto" não é gerenciado. Ou seja, a seguinte linha:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... vazará memória se um acompanhamento deletenão for executado no momento adequado.

auto_ptr

A fim de minimizar esses casos, std::auto_ptr<>foi introduzido. Devido às limitações do C ++ anteriores ao padrão de 2011, no entanto, ainda é muito fácil auto_ptrvazar memória. É suficiente para casos limitados, como este, no entanto:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Um de seus casos de uso mais fracos é em contêineres. Isso ocorre porque, se uma cópia de uma auto_ptr<>é feita e a cópia antiga não é redefinida com cuidado, o contêiner pode excluir o ponteiro e perder dados.

unique_ptr

Como substituição, o C ++ 11 introduziu std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Tal unique_ptr<>será limpo corretamente, mesmo quando for passado entre as funções. Ele faz isso representando semanticamente a "propriedade" do ponteiro - o "proprietário" a limpa. Isso o torna ideal para uso em contêineres:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Ao contrário auto_ptr<>, unique_ptr<>é bem-comportado aqui e, quando o vectorredimensionamento, nenhum dos objetos será excluído acidentalmente enquanto a vectorcópia é armazenada em backup.

shared_ptr e weak_ptr

unique_ptr<>é útil, com certeza, mas há casos em que você deseja que duas partes da sua base de código possam se referir ao mesmo objeto e copiar o ponteiro, mantendo a limpeza adequada garantida. Por exemplo, uma árvore pode ficar assim, ao usar std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Nesse caso, podemos até manter várias cópias de um nó raiz, e a árvore será limpa adequadamente quando todas as cópias do nó raiz forem destruídas.

Isso funciona porque cada um deles shared_ptr<>mantém não apenas o ponteiro para o objeto, mas também uma contagem de referência de todos os shared_ptr<>objetos que se referem ao mesmo ponteiro. Quando um novo é criado, a contagem aumenta. Quando um é destruído, a contagem diminui. Quando a contagem chega a zero, o ponteiro é deleted.

Portanto, isso apresenta um problema: Estruturas com ligações duplas terminam em referências circulares. Digamos que queremos adicionar um parentponteiro à nossa árvore Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Agora, se removermos um Node, há uma referência cíclica a ele. Nunca será deleted porque sua contagem de referência nunca será zero.

Para resolver esse problema, você usa um std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Agora, tudo funcionará corretamente e a remoção de um nó não deixará referências presas ao nó pai. No entanto, torna a caminhada na árvore um pouco mais complicada:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

Dessa forma, você pode bloquear uma referência ao nó e ter uma garantia razoável de que ele não desaparecerá enquanto você estiver trabalhando nele, já que está segurando shared_ptr<>nele.

make_shared e make_unique

Agora, existem alguns problemas menores shared_ptr<>e unique_ptr<>que devem ser resolvidos. As duas linhas a seguir têm um problema:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Se thrower()lançar uma exceção, as duas linhas vazarão memória. E mais do que isso, shared_ptr<>mantém a contagem de referência longe do objeto para o qual aponta e isso pode significar uma segunda alocação). Isso geralmente não é desejável.

O C ++ 11 fornece std::make_shared<>()e o C ++ 14 fornece std::make_unique<>()para resolver esse problema:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Agora, nos dois casos, mesmo se thrower()lançar uma exceção, não haverá vazamento de memória. Como bônus, make_shared<>()tem a oportunidade de criar sua contagem de referência no mesmo espaço de memória que seu objeto gerenciado, que pode ser mais rápido e economizar alguns bytes de memória, oferecendo uma exceção de garantia de segurança!

Notas sobre Qt

Deve-se notar, no entanto, que o Qt, que deve suportar compiladores anteriores ao C ++ 11, possui seu próprio modelo de coleta de lixo: muitos QObjects possuem um mecanismo no qual eles serão destruídos corretamente sem a necessidade do usuário para deleteeles.

Não sei como QObjectse comportará quando gerenciado por ponteiros gerenciados pelo C ++ 11, portanto não posso dizer que shared_ptr<QDialog>é uma boa idéia. Não tenho experiência suficiente com o Qt para ter certeza, mas acredito que o Qt5 foi ajustado para este caso de uso.

desvanecer-se
fonte
1
@ Zilators: Observe o meu comentário adicionado sobre Qt. A resposta à sua pergunta sobre se os três ponteiros devem ser gerenciados depende se os objetos Qt se comportarão bem.
28515 greyfade
2
"ambos fazem alocação separada para segurar o ponteiro"? Não, unique_ptr nunca aloca nada extra, apenas shared_ptr deve alocar um objeto de contagem de referência + alocador. "ambas as linhas vão vazar memória"? não, apenas poderia, nem mesmo uma garantia de mau comportamento.
Deduplicator
1
@ Reduplicador: Minha redação deve não ter sido clara: o shared_ptrobjeto é um objeto separado - uma alocação separada - do newobjeto ed. Eles existem em diferentes locais. make_sharedtem a capacidade de reuni-los no mesmo local, o que melhora a localidade do cache, entre outras coisas.
Greyfade #
2
@greyfade: Nononono. shared_ptré um objeto. E para gerenciar um objeto, ele deve alocar um objeto (contagens de referência (fraco + forte) + destruidor). make_sharedpermite alocar isso e o objeto gerenciado como uma peça. unique_ptrnão os usa, portanto, não há vantagem correspondente, além de garantir que o objeto seja sempre de propriedade do ponteiro inteligente. Como um aparte, pode-se ter um shared_ptrque possua um objeto subjacente e represente um nullptr, ou que não possua e represente um ponteiro não nulo.
Deduplicator
1
Eu olhei para ela e parece haver uma confusão geral sobre o que a pessoa shared_ptrfaz: 1. Compartilha a propriedade de algum objeto (representado por um objeto interno alocado dinamicamente, com uma contagem de referência fraca e forte, além de um deleter) . 2. Ele contém um ponteiro. Essas duas partes são independentes. make_uniquee make_sharedambos garantem que o objeto alocado seja colocado com segurança em um ponteiro inteligente. Além disso, make_sharedpermite alocar o objeto de propriedade e o ponteiro gerenciado juntos.
Deduplicator