Eu estava apenas assistindo as transmissões "Going Native 2012" e notei a discussão sobre std::shared_ptr
. Fiquei um pouco surpreso ao ouvir a visão um tanto negativa de Bjarne std::shared_ptr
e seu comentário de que ele deveria ser usado como "último recurso" quando a vida de um objeto é incerta (o que acredito, segundo ele, não deve ser o caso).
Alguém se importaria de explicar isso com um pouco mais de profundidade? Como podemos programar sem std::shared_ptr
e ainda gerenciar a vida útil dos objetos de maneira segura ?
c++
c++11
smart-pointer
ronag
fonte
fonte
Respostas:
Se você puder evitar a propriedade compartilhada, seu aplicativo será mais simples e fácil de entender e, portanto, menos suscetível a erros introduzidos durante a manutenção. Modelos de propriedade complexos ou pouco claros tendem a levar a acoplamentos difíceis de seguir de diferentes partes do aplicativo através de um estado compartilhado que pode não ser facilmente rastreável.
Diante disso, é preferível usar objetos com duração de armazenamento automático e ter subobjetos de "valor". Caso contrário,
unique_ptr
pode ser uma boa alternativa emshared_ptr
ser - se não um último recurso - de alguma forma na lista de ferramentas desejáveis.fonte
O mundo em que Bjarne vive é muito ... acadêmico, por falta de um termo melhor. Se o seu código puder ser projetado e estruturado de modo que os objetos tenham hierarquias relacionais muito deliberadas, de modo que os relacionamentos de propriedade sejam rígidos e inflexíveis, o código fluirá em uma direção (de alto nível para baixo), e os objetos só falarão com os de baixo nível. hierarquia, então você não encontrará muita necessidade
shared_ptr
. É algo que você usa nessas raras ocasiões em que alguém tem que quebrar as regras. Mas, caso contrário, você pode colocar tudo emvector
s ou outras estruturas de dados que usam semântica de valorunique_ptr
es para coisas que você deve alocar individualmente.Embora esse seja um ótimo mundo para se viver, não é o que você pode fazer o tempo todo. Se você não pode organizar seu código dessa maneira, porque o design do sistema que você está tentando criar significa que é impossível (ou apenas profundamente desagradável), você se encontrará cada vez mais necessitando de propriedade compartilhada de objetos. .
Nesse sistema, manter ponteiros nus não é exatamente perigoso, mas levanta questões. O melhor de tudo
shared_ptr
é que ele fornece garantias sintáticas razoáveis sobre a vida útil do objeto. Pode ser quebrado? Claro. Mas as pessoas também podemconst_cast
coisas; cuidados básicos e alimentação deshared_ptr
devem fornecer qualidade de vida razoável para objetos alocados cuja propriedade deve ser compartilhada.Depois, há
weak_ptr
s, que não podem ser usados na ausência de ashared_ptr
. Se o seu sistema estiver rigidamente estruturado, você poderá armazenar um ponteiro nu para algum objeto, seguro de que a estrutura do aplicativo garante que o objeto apontado sobreviverá a você. Você pode chamar uma função que retorne um ponteiro para algum valor interno ou externo (localize o objeto chamado X, por exemplo). No código estruturado adequadamente, essa função só estaria disponível se a vida útil do objeto exceder a sua; portanto, é bom armazenar o ponteiro nu no seu objeto.Como essa rigidez nem sempre é possível em sistemas reais, você precisa de uma maneira de garantir razoavelmente a vida útil. Às vezes, você não precisa de propriedade total; às vezes, você só precisa saber quando o ponteiro é ruim ou bom. É aí que
weak_ptr
entra. Houve casos em que eu poderia ter usado umunique_ptr
orboost::scoped_ptr
, mas tive que usar umshared_ptr
porque eu precisava especificamente fornecer a alguém um ponteiro "volátil". Um ponteiro com vida útil indeterminada, e eles poderiam consultar quando esse ponteiro foi destruído.Uma maneira segura de sobreviver quando o estado do mundo é indeterminado.
Isso poderia ter sido feito por alguma chamada de função para obter o ponteiro, em vez de via
weak_ptr
? Sim, mas isso poderia ser mais facilmente quebrado. Uma função que retorna um ponteiro nu não tem como sugerir sintaticamente que o usuário não faça algo como armazenar esse ponteiro a longo prazo. O retorno de umshared_ptr
também torna muito fácil para alguém simplesmente armazená-lo e potencialmente prolongar a vida útil de um objeto. Retornar umweak_ptr
no entanto sugere fortemente que armazenar o queshared_ptr
você recebelock
é uma ... idéia duvidosa. Isso não impedirá que você faça isso, mas nada no C ++ o impede de quebrar o código.weak_ptr
fornece alguma resistência mínima ao fazer a coisa natural.Agora, isso não quer dizer que
shared_ptr
não possa ser usado em excesso ; certamente pode. Especialmente antesunique_ptr
, havia muitos casos em que eu apenas usava umboost::shared_ptr
porque precisava passar um ponteiro RAII ou colocá-lo em uma lista. Sem movimento semântica eunique_ptr
,boost::shared_ptr
era a única solução real.E você pode usá-lo em locais onde é desnecessário. Como mencionado acima, a estrutura de código adequada pode eliminar a necessidade de alguns usos de
shared_ptr
. Mas se o seu sistema não puder ser estruturado como tal e ainda fizer o que for necessário,shared_ptr
será de uso significativo.fonte
shared_ptr
é ótimo para sistemas em que o c ++ se integra à linguagem de script como python. Usandoboost::python
, a contagem de referência no lado c ++ e python coopera muito; qualquer objeto do c ++ ainda pode ser mantido em python e não morrerá.shared_ptr
. Ambos usam suas próprias implementações deintrusive_ptr
. Eu só trazer isso à tona porque ambos são exemplos do mundo real de grandes aplicações escritas em C ++shared_ptr
aplica igualmente aintrusive_ptr
: ele está se opondo a todo o conceito de propriedade compartilhada, não a qualquer grafia específica do conceito. Assim, para os fins desta questão, esses são dois exemplos do mundo real de grandes aplicações que não usamshared_ptr
. (E, além do mais, eles demonstram queshared_ptr
é útil mesmo quando ele não permiteweak_ptr
.)Eu não acredito que já usei
std::shared_ptr
.Na maioria das vezes, um objeto é associado a alguma coleção, à qual pertence durante toda a sua vida útil. Nesse caso, você pode simplesmente usar
whatever_collection<o_type>
orwhatever_collection<std::unique_ptr<o_type>>
, essa coleção sendo um membro de um objeto ou uma variável automática. Obviamente, se você não precisar de um número dinâmico de objetos, poderá usar apenas uma matriz automática de tamanho fixo.Nenhuma iteração através da coleção ou qualquer outra operação no objeto requer uma função auxiliar para compartilhar a propriedade ... ela usa o objeto, depois retorna e o chamador garante que o objeto permaneça ativo durante toda a chamada . Esse é de longe o contrato mais usado entre o chamador e o chamado.
Nicol Bolas comentou que "se algum objeto se apega a um ponteiro nu e esse objeto morre ... oops". e "Os objetos precisam garantir que o objeto passe pela vida desse objeto. Somente isso
shared_ptr
pode ser feito".Eu não compro esse argumento. Pelo menos não isso
shared_ptr
resolve esse problema. Sobre:Como a coleta de lixo, o uso padrão de
shared_ptr
incentiva o programador a não pensar no contrato entre objetos ou entre a função e o chamador. É necessário pensar em pré-condições e pós-condições corretas, e a vida útil do objeto é apenas um pedacinho dessa torta maior.Objetos não "morrem", algum pedaço de código os destrói. E jogar
shared_ptr
o problema em vez de descobrir o contrato de chamada é uma segurança falsa.fonte
shared_ptr
eweak_ptr
foram projetados para evitar. Bjarne tenta viver em um mundo onde tudo tem uma vida agradável e explícita, e tudo é construído em torno disso. E se você pode construir esse mundo, ótimo. Mas não é assim que é no mundo real. Os objetos precisam garantir que o objeto passe pela vida desse objeto. Sóshared_ptr
pode fazer isso.shared_ptr
apenas mitiga uma modificação externa específica, e nem mesmo a mais comum. E não é responsabilidade do objeto garantir que sua vida útil esteja correta, se o contrato de chamada de função especificar o contrário.unique_ptr
, expressando que existe apenas um ponteiro para o objeto e ele possui propriedade.shared_ptr
, ele ainda deve retornar aunique_ptr
. A conversão deunique_ptr
parashared_ptr
é fácil, mas o inverso é logicamente impossível.Prefiro não pensar em termos absolutos (como "último recurso"), mas em relação ao domínio do problema.
O C ++ pode oferecer várias maneiras diferentes de gerenciar a vida útil. Alguns deles tentam re-conduzir os objetos de maneira acionada por pilha. Alguns outros tentam escapar dessa limitação. Alguns deles são "literais", outros são aproximações.
Na verdade você pode:
Person
tendo o mesmoname
são a mesma pessoa (melhor: duas representações de uma mesma pessoa ). A vida útil é concedida pela pilha da máquina, o que não é importante para o programa (afinal, uma pessoa é seu nome , não importa o que aPerson
esteja carregando)std::unique_ptr
(você pode pensar nisso como um vetor com tamanho 1). Novamente, você admite que o objeto começa a existir (e termina sua existência) antes (depois) da estrutura de dados a que se refere.O ponto fraco desses métodos é que os tipos e quantidades de objetos não podem variar durante a execução de chamadas mais profundas no nível da pilha em relação ao local em que foram criadas. Todas essas técnicas "fracassam" em todas as situações em que a criação e a exclusão de objetos são consequências das atividades do usuário, de modo que o tipo de tempo de execução do objeto não é conhecido em tempo de compilação e pode haver estruturas excessivas referentes aos objetos O usuário está solicitando a remoção de uma chamada de função no nível da pilha mais profunda. Nesse caso, você deve:
O C ++ isteslf não possui nenhum mecanismo nativo para monitorar esse evento (
while(are_they_needed)
); portanto, você precisa se aproximar de:Indo para a primeira solução até a última, a quantidade de estrutura de dados auxiliar necessária para gerenciar a vida útil do objeto aumenta, conforme o tempo gasto para organizá-lo e mantê-lo.
O coletor de lixo tem custo, shared_ptr tem menos, unique_ptr ainda menos e os objetos gerenciados por pilha têm muito poucos.
É
shared_ptr
o "último recurso" ?. Não, não é: o último recurso são os coletores de lixo.shared_ptr
é realmente ostd::
último recurso proposto. Mas pode ser a solução certa, se você estiver na situação que expliquei.fonte
A única coisa mencionada por Herb Sutter em uma sessão posterior é que toda vez que você copia um,
shared_ptr<>
há um incremento / decremento intertravado que deve ocorrer. No código multiencadeado em um sistema multinúcleo, a sincronização de memória não é insignificante. Dada a escolha, é melhor usar um valor de pilha ou aunique_ptr<>
e passar referências ou ponteiros brutos.fonte
shared_ptr
por lvalue ou referência rvalue ...shared_ptr
como se fosse a bala de prata que resolverá todos os seus problemas de vazamento de memória apenas porque está no padrão. É uma armadilha tentadora, mas ainda é importante estar ciente da propriedade dos recursos e, a menos que essa propriedade seja compartilhada, ashared_ptr<>
não é a melhor opção.Não me lembro se o último "recurso" foi a palavra exata que ele usou, mas acredito que o significado real do que ele disse era a última "escolha": dadas condições claras de propriedade; unique_ptr, fraco_ptr, shared_ptr e até ponteiros nus têm seu lugar.
Uma coisa em que todos concordaram é que estamos (desenvolvedores, autores de livros etc.) todos na "fase de aprendizado" do C ++ 11 e padrões e estilos estão sendo definidos.
Como exemplo, Herb explicou que devemos esperar novas edições de alguns livros seminais em C ++, como Effective C ++ (Meyers) e C ++ Coding Standards (Sutter & Alexandrescu), daqui a alguns anos, enquanto a experiência e as melhores práticas do setor em C ++ 11 pan fora.
fonte
Eu acho que ele está entendendo que está se tornando comum que todos escrevam shared_ptr sempre que possam ter escrito um ponteiro padrão (como uma espécie de substituição global), e que ele está sendo usado como um cop-out em vez de realmente projetar ou pelo menos planejamento para criação e exclusão de objetos.
A outra coisa que as pessoas esquecem (além do gargalo de bloqueio / atualização / desbloqueio mencionado no material acima) é que o shared_ptr sozinho não resolve problemas de ciclo. Você ainda pode vazar recursos com shared_ptr:
O Objeto A, contém um ponteiro compartilhado para outro Objeto A O Objeto B cria A a1 e A a2 e atribui a a1.otherA = a2; e a2.outroA = a1; Agora, os ponteiros compartilhados do objeto B usados para criar a1, a2 ficam fora do escopo (digamos, no final de uma função). Agora você tem um vazamento - ninguém mais se refere a a1 e a2, mas eles se referem um ao outro, de modo que suas contagens de referências são sempre 1 e você vazou.
Esse é o exemplo simples, quando isso ocorre em código real, geralmente acontece de maneiras complicadas. Existe uma solução com o fraco_ptr, mas agora muitas pessoas fazem o shared_ptr em todos os lugares e nem sequer sabem do problema de vazamento ou mesmo do fraco_ptr.
Para finalizar: acho que os comentários referenciados pelo OP se resumem a isso:
Independentemente do idioma em que você está trabalhando (gerenciado, não gerenciado ou algo intermediário com contagens de referência como shared_ptr), você precisa entender e decidir intencionalmente a criação, a duração e a destruição de objetos.
edit: mesmo que isso signifique "desconhecido, preciso usar um shared_ptr", você ainda pensou nisso e está fazendo isso intencionalmente.
fonte
Responderei da minha experiência com o Objective-C, uma linguagem em que todos os objetos são contados e alocados na pilha de referência. Por ter uma maneira de tratar objetos, as coisas são muito mais fáceis para o programador. Isso permitiu a definição de regras padrão que, quando aderidas, garantem robustez do código e nenhum vazamento de memória. Também possibilitou a otimização inteligente do compilador, como o recente ARC (contagem automática de referência).
O que quero dizer é que shared_ptr deve ser sua primeira opção e não o último recurso. Use a contagem de referência por padrão e outras opções apenas se tiver certeza do que está fazendo. Você será mais produtivo e seu código será mais robusto.
fonte
Vou tentar responder à pergunta:
O C ++ possui um grande número de maneiras diferentes de executar a memória, por exemplo:
struct A { MyStruct s1,s2; };
vez de shared_ptr no escopo da classe. Isso é apenas para programadores avançados, pois requer que você entenda como as dependências funcionam e requer capacidade de controlar dependências o suficiente para restringi-las a uma árvore. A ordem das classes no arquivo de cabeçalho é um aspecto importante disso. Parece que esse uso já é comum nos tipos nativos do c ++, mas é usado com classes definidas pelo programador, mas parece ser menos usado devido a esses problemas de dependência e ordem das classes. Esta solução também tem problemas com sizeof. Os programadores veem problemas nisso como um requisito para usar declarações futuras ou #includes desnecessários e, portanto, muitos programadores voltarão à solução inferior de ponteiros e posteriormente ao shared_ptr.MyClass &find_obj(int i);
+ clone () em vez deshared_ptr<MyClass> create_obj(int i);
. Muitos programadores desejam criar fábricas para criar novos objetos. shared_ptr é ideal para esse tipo de uso. O problema é que ele já assume uma solução complexa de gerenciamento de memória usando alocação de heap / free store, em vez de uma pilha mais simples ou uma solução baseada em objeto. Uma boa hierarquia de classes C ++ suporta todos os esquemas de gerenciamento de memória, não apenas um deles. A solução baseada em referência pode funcionar se o objeto retornado for armazenado dentro do objeto que contém, em vez de usar a variável de escopo da função local. A passagem da propriedade da fábrica para o código do usuário deve ser evitada. Copiar o objeto depois de usar o find_obj () é uma boa maneira de lidar com isso - os construtores de cópias normais e o construtor normal (de classe diferente) com o parâmetro de referência ou clone () para objetos polimórficos podem lidar com isso.fonte
unique_ptr
é mais adequado para fábricas. Você pode transformar umunique_ptr
em umshared_ptr
, mas é logicamente impossível seguir na outra direção.