Usando ponteiros inteligentes para os alunos

159

Estou tendo problemas para entender o uso de ponteiros inteligentes como membros da classe em C ++ 11. Eu li muito sobre ponteiros inteligentes e acho que entendo como unique_ptre shared_ptr/ weak_ptrtrabalho em geral. O que eu não entendo é o uso real. Parece que todo mundo recomenda usar unique_ptro caminho a percorrer quase o tempo todo. Mas como eu implementaria algo assim:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Digamos que eu gostaria de substituir os ponteiros por ponteiros inteligentes. A unique_ptrnão funcionaria por causa de getDevice(), certo? Então é nessa hora que eu uso shared_ptre weak_ptr? Não tem como usar unique_ptr? Parece-me que, na maioria dos casos, shared_ptrfaz mais sentido, a menos que eu esteja usando um ponteiro em um escopo muito pequeno?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Aquele é o caminho para ir? Muito obrigado!

michaelk
fonte
4
Ajuda a ser realmente claro quanto à vida útil, propriedade e possíveis valores nulos. Por exemplo, tendo passado devicepara o construtor de settings, você ainda pode se referir a ele no escopo da chamada ou apenas via settings? Se o último, unique_ptré útil. Além disso, você tem um cenário em que o valor de retorno getDevice()é null. Caso contrário, basta retornar uma referência.
26413 Keith
2
Sim, a shared_ptrestá correto em 8/10 casos. Os outros 2/10 são divididos entre unique_ptre weak_ptr. Além disso, weak_ptré geralmente usado para quebrar referências circulares; Não tenho certeza de que seu uso seria considerado correto.
Collin Dauphinee
2
Primeiro de tudo, que propriedade você deseja para o devicemembro de dados? Você primeiro tem que decidir isso.
juanchopanza
1
Ok, entendo que, como chamador, eu poderia usar um unique_ptre renunciar à propriedade ao chamar o construtor, se eu souber que não precisarei mais dele por enquanto. Mas, como designer da Settingsclasse, não sei se o chamador também quer manter uma referência. Talvez o dispositivo seja usado em muitos lugares. Ok, talvez esse seja exatamente o seu ponto. Nesse caso, eu não seria o único proprietário e é aí que eu usaria o shared_ptr, eu acho. E: pontos inteligentes substituem ponteiros, mas não referências, certo?
MichaelK
this-> device = device; Use também listas de inicialização.
Nils

Respostas:

202

A unique_ptrnão funcionaria por causa de getDevice(), certo?

Não, não necessariamente. O importante aqui é determinar a política de propriedade apropriada para o seu Deviceobjeto, ou seja, quem será o proprietário do objeto apontado pelo seu ponteiro (inteligente).

Será a instância do Settingsobjeto sozinha ? O Deviceobjeto terá que ser destruído automaticamente quando o Settingsobjeto for destruído ou deve sobreviver a ele?

No primeiro caso, std::unique_ptré o que você precisa, pois ele torna Settingso único (único) proprietário do objeto apontado e o único objeto responsável por sua destruição.

Sob essa suposição, getDevice()deve retornar um ponteiro de observação simples (observar ponteiros são ponteiros que não mantêm o objeto apontado vivo). O tipo mais simples de observação de ponteiro é um ponteiro bruto:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ NOTA 1: Você pode estar se perguntando por que estou usando ponteiros brutos aqui, quando todo mundo fica dizendo que os ponteiros brutos são ruins, inseguros e perigosos. Na verdade, esse é um aviso precioso, mas é importante colocá-lo no contexto correto: ponteiros brutos são ruins quando usados ​​para executar o gerenciamento manual de memória , ou seja, alocar e desalocar objetos através de newe delete. Quando usado puramente como um meio de obter semântica de referência e passar ponteiros não-proprietários, observando ponteiros, não há nada intrinsecamente perigoso em ponteiros brutos, exceto talvez pelo fato de que se deve tomar cuidado para não desreferenciar um ponteiro pendente. - NOTA FINAL 1 ]

[ NOTA 2: Como surgiu nos comentários, neste caso específico em que a propriedade é única e o objeto de propriedade sempre está presente (ou seja, o membro de dados interno devicenunca estará presente nullptr), a função getDevice()poderia (e talvez devesse) retornar uma referência em vez de um ponteiro. Embora isso seja verdade, eu decidi retornar um ponteiro bruto aqui, porque eu quis dizer que essa seria uma resposta curta que poderia ser generalizada para o caso em que devicepoderia estar nullptre mostrar que os ponteiros não processados ​​são bons, desde que não sejam usados ​​para gerenciamento manual de memória. - NOTA FINAL 2 ]


A situação é radicalmente diferente, é claro, se o seu Settingsobjeto não tiver a propriedade exclusiva do dispositivo. Este poderia ser o caso, por exemplo, se a destruição do Settingsobjeto não implicasse também a destruição do Deviceobjeto apontado .

Isso é algo que somente você, como designer do seu programa, pode dizer; pelo exemplo que você fornece, é difícil para mim dizer se esse é o caso ou não.

Para ajudá-lo a descobrir, você pode se perguntar se existem outros objetos além dos Settingsque têm o direito de manter o Deviceobjeto vivo enquanto mantiverem um ponteiro para ele, em vez de serem apenas observadores passivos. Se esse for realmente o caso, você precisará de uma política de propriedade compartilhada , que é o que std::shared_ptroferece:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Observe que weak_ptré um ponteiro de observação , não um ponteiro proprietário - em outras palavras, ele não mantém o objeto apontado vivo se todos os outros ponteiros proprietários do objeto apontado ficarem fora do escopo.

A vantagem de weak_ptrum ponteiro bruto comum é que você pode dizer com segurança se weak_ptrestá danificado ou não (por exemplo, se está apontando para um objeto válido ou se o objeto apontado originalmente foi destruído). Isso pode ser feito chamando a expired()função de membro no weak_ptrobjeto.

Andy Prowl
fonte
4
@LKK: Sim, correto. A weak_ptré sempre uma alternativa aos ponteiros de observação brutos. É mais seguro em um sentido, porque você pode verificar se está oscilando antes de desreferenciá-lo, mas também vem com alguma sobrecarga. Se você pode facilmente garantir que você não está indo para excluir a referência um ponteiro pendurado, então você deve estar bem com a observação ponteiros crus
Andy Prowl
6
No primeiro caso, provavelmente seria ainda melhor deixar getDevice()retornar uma referência, não é? Portanto, o chamador não precisaria procurar nullptr.
vobject
5
@ chico: Não sei o que você quer dizer. auto myDevice = settings.getDevice()irá criar uma nova instância do tipo Devicechamada myDevicee copiá-la a partir daquela referenciada pela referência que getDevice()retorna. Se você quer myDeviceser uma referência, precisa fazer auto& myDevice = settings.getDevice(). Portanto, a menos que esteja faltando alguma coisa, estamos de volta à mesma situação que tivemos sem usar auto.
Andy Prowl 27/03
2
@ Performance: Como você não deseja ceder a propriedade do objeto - entregar um modificável unique_ptra um cliente abre a possibilidade de o cliente sair dele, adquirindo propriedade e deixando um ponteiro nulo (exclusivo).
Andy Prowl
7
@ Performance: Embora isso impeça que um cliente se mova (a menos que seja um cientista louco e interessado em const_cast), eu pessoalmente não faria isso. Ele expõe um detalhe de implementação, ou seja, o fato de que a propriedade é única e realizada por meio de a unique_ptr. Eu vejo as coisas desta maneira: se você deseja / precisa passar / devolver a propriedade, passe / retorne um ponteiro inteligente ( unique_ptrou shared_ptr, dependendo do tipo de propriedade). Se você não quiser / precisar passar / retornar a propriedade, use um constponteiro ou referência (adequadamente qualificado), principalmente dependendo se o argumento pode ser nulo ou não.
Andy Prowl
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptré usado apenas para loops de referência. O gráfico de dependência deve ser um gráfico direcionado acíclico. Nos ponteiros compartilhados, existem 2 contagens de referência: 1 para se shared_ptr1 para todos os ponteiros ( shared_ptre weak_ptr). Quando todos os shared_ptrs são removidos, o ponteiro é excluído. Quando o ponteiro é necessário weak_ptr, lockdeve ser usado para obter o ponteiro, se ele existir.

Naszta
fonte
Portanto, se entendi sua resposta corretamente, os ponteiros inteligentes substituem os ponteiros brutos, mas não necessariamente as referências?
MichaelK
Na verdade, existem duas contagens de referência em a shared_ptr? Você pode, por favor, explicar o porquê? Tanto quanto eu entendo, weak_ptrnão precisa ser contado porque simplesmente cria um novo shared_ptrao operar no objeto (se o objeto subjacente ainda existir).
Björn Pollex 27/03
@ BjörnPollex: Criei um pequeno exemplo para você: link . Eu não implementei tudo apenas os construtores de cópia e lock. A versão boost também é segura para threads na contagem de referência ( deleteé chamada apenas uma vez).
Naszta
@ Naszta: Seu exemplo mostra que é possível implementar isso usando duas contagens de referência, mas sua resposta sugere que isso é necessário , o que não acredito que seja. Você poderia esclarecer isso na sua resposta?
Björn Pollex
1
@ BjörnPollex, weak_ptr::lock()para saber se o objeto expirou, ele deve inspecionar o "bloco de controle" que contém a primeira contagem de referência e ponteiro para o objeto, para que o bloco de controle não seja destruído enquanto ainda houver weak_ptrobjetos em uso, portanto, o número de weak_ptrobjetos deve ser rastreado, que é o que a segunda contagem de referência faz. O objeto é destruído quando a primeira contagem de referência cai para zero, o bloco de controle é destruído quando a segunda contagem de referência cai para zero.
Jonathan Wakely 31/03