std :: shared_ptr como último recurso?

58

Eu estava assistindo apenas aos fluxos "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 sobre std::shared_ptr e seu comentário de que ela deveria ser usada como um "último recurso" quando a vida útil de um objeto é incerta (que, segundo ele, caso).

Alguém se importaria de explicar isso com um pouco mais de profundidade? Como podemos programar sem std::shared_ptr e ainda gerenciar os tempos de vida do objeto de uma maneira segura ?

    
por ronag 04.02.2012 / 15:47
fonte

9 respostas

56

Se você puder evitar a propriedade compartilhada, seu aplicativo será mais simples e fácil de entender e, portanto, menos suscetível a bugs introduzidos durante a manutenção. Modelos de propriedade complexos ou pouco claros tendem a dificultar o acoplamento de diferentes partes do aplicativo por meio de um estado compartilhado que pode não ser facilmente rastreável.

Por isso, é preferível usar objetos com duração de armazenamento automático e ter sub-objetos de "valor". Caso contrário, unique_ptr pode ser uma boa alternativa, com shared_ptr sendo - se não for um último recurso - algum caminho na lista de ferramentas desejáveis.

    
por 04.02.2012 / 16:00
fonte
47

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 as relações de propriedade sejam rígidas e inflexíveis, o código flui em uma direção (de alto nível para baixo) e os objetos só falam com os mais baixos a hierarquia, então você não encontrará muita necessidade de shared_ptr . É algo que você usa nas raras ocasiões em que alguém tem que quebrar as regras. Caso contrário, você pode colocar tudo em vector s ou outras estruturas de dados que usam semântica de valores, e unique_ptr s para coisas que você tem que alocar individualmente.

Embora seja um ótimo mundo para se viver, não é o que você consegue fazer o tempo todo. Se você não puder organizar seu código dessa forma, porque o design do sistema que você está tentando fazer significa que é impossível (ou apenas profundamente desagradável), então você vai se encontrar precisando propriedade compartilhada de objetos cada vez mais.

Nesse sistema, segurar ponteiros nus não é ... exatamente perigoso, mas levanta questões. O melhor de shared_ptr é que ele fornece garantias sintáticas razoáveis sobre o tempo de vida do objeto. Pode ser quebrado? Claro. Mas as pessoas também podem const_cast coisas; cuidados básicos e alimentação de shared_ptr devem fornecer qualidade de vida razoável para os objetos alocados cuja propriedade deve ser compartilhada.

Em seguida, existem weak_ptr s, que não podem ser usados na ausência de shared_ptr . Se o seu sistema estiver rigidamente estruturado, você poderá armazenar um ponteiro nu em algum objeto, sabendo que a estrutura do aplicativo garante que o objeto apontado sobreviverá a você. Você pode chamar uma função que retorna um ponteiro para algum valor interno ou externo (encontre um objeto chamado X, por exemplo). No código adequadamente estruturado, essa função só estaria disponível para você se o tempo de vida do objeto fosse superior ao seu; assim, armazenar esse ponteiro nu em seu objeto é bom.

Como essa rigidez nem sempre é possível de ser alcançada em sistemas reais, você precisa de alguma maneira de garantir a vida útil. Às vezes, você não precisa de propriedade total; às vezes, você só precisa saber quando o ponteiro está bom ou ruim. É aí que entra weak_ptr . Houve casos em que poderia ter usado unique_ptr ou boost::scoped_ptr , mas tive que usar shared_ptr porque especificamente

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 mais facilmente ser quebrado. Uma função que retorna um ponteiro nu não tem como sintaticamente sugerir que o usuário não faça algo como armazenar esse ponteiro a longo prazo. Retornar um shared_ptr também torna muito fácil para alguém simplesmente armazená-lo e potencialmente prolongar o tempo de vida de um objeto. No entanto, retornar um weak_ptr sugere que armazenar o shared_ptr obtido de lock é uma ... idéia duvidosa. Isso não vai impedi-lo de fazer isso, mas nada em C ++ impede você 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 pode ser usado em excesso ; certamente pode. Especialmente antes de unique_ptr , houve muitos casos em que usei apenas boost::shared_ptr porque precisava passar um ponteiro RAII ou colocá-lo em uma lista. Sem mover a semântica e unique_ptr , boost::shared_ptr era a única solução real.

E você pode usá-lo em lugares onde é completamente desnecessário. Como dito acima, a estrutura correta do código pode eliminar a necessidade de alguns usos de shared_ptr . Mas se o seu sistema não puder ser estruturado como tal e ainda assim fizer o que for necessário, shared_ptr será de uso significativo.

    
por 04.02.2012 / 17:11
fonte
37

Eu não acredito que já usei std::shared_ptr .

Na maioria das vezes, um objeto é associado a alguma coleção, à qual pertence por toda a sua vida útil. Nesse caso, você pode usar apenas whatever_collection<o_type> ou whatever_collection<std::unique_ptr<o_type>> , sendo essa coleção um membro de um objeto ou uma variável automática. É claro que, se você não precisasse de um número dinâmico de objetos, você poderia usar 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 ... ele usa o objeto, em seguida, retorna e o chamador garante que o objeto permanece ativo durante toda a chamada Este é de longe o contrato mais usado entre o chamador e o chamado.

Nicol Bolas comentou que "Se algum objeto se mantiver em um ponteiro nu e esse objeto morrer ... oops." e "Objetos precisam garantir que o objeto passe pela vida desse objeto. Somente shared_ptr pode fazer isso."

Eu não compro esse argumento. Pelo menos não que shared_ptr resolva esse problema. O que acontece:

  • Se alguma tabela de hash for mantida em um objeto e o hashcode desse objeto for alterado ... opa.
  • Se alguma função for iterar um vetor e um elemento for inserido nesse vetor ... opa.

Como a coleta de lixo, o uso padrão de shared_ptr incentiva o programador a não pensar no contrato entre os objetos ou entre a função e o chamador. Pensar em pré-condições e pós-condições corretas é necessário, e o tempo de vida do objeto é apenas uma pequena parte desse bolo maior.

Objetos não "morrem", algum trecho de código os destrói. E jogar shared_ptr no problema em vez de descobrir o contrato de chamada é uma falsa segurança.

    
por 04.02.2012 / 16:02
fonte
16

Eu 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 uma 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:

  1. use semântica de valor puro . Funciona para objetos relativamente pequenos onde o que é importante são "valores" e não "identidades", onde você pode assumir que dois Person com o mesmo name são os mesmos pessoa (melhor: dois representação de uma mesma pessoa ). A vida útil é concedida pela pilha da máquina, e, essencialmente, não importa para o programa (uma vez que pessoa é nome , não importa o que Person esteja carregando isso)
  2. use objetos alocados na pilha e referências ou ponteiros relacionados: permite o polimorfismo e concede o tempo de vida do objeto. Não há necessidade de "ponteiros inteligentes", pois você garante que nenhum objeto possa ser "apontado" por estruturas que deixam na pilha mais tempo do que o objeto para o qual apontam (primeiro crie o objeto e depois as estruturas que se referem a ele). >
  3. use objetos alocados na pilha gerenciada da pilha : é isso que std :: vector e todos os contêineres fazem e wat 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 referem.

A fraqueza deste mehtods é que os tipos e quantidades de objetos não podem variar durante a execução de chamadas de nível de pilha mais profundas em relação ao local onde elas são criadas. Todas essas técnicas "falham" sua força em toda a situação em que a criação e a exclusão de objetos são conseqüência 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 em excesso referindo-se a objetos o usuário está pedindo para remover de uma chamada de função de nível de pilha mais profunda. Nesse caso, você precisa:

  • introduza alguma disciplina sobre o gerenciamento de objetos e estruturas de referência relacionadas ou ...
  • vai de alguma forma para o lado escuro de "escapar da vida útil baseada em pilha pura": objeto deve sair independentemente das funções que os criaram. E deve sair ... até que eles sejam necessários .

O C ++ isteslf não possui nenhum mecanismo nativo para monitorar esse evento ( while(are_they_needed) ), portanto você tem que aproximar com:

  1. use a propriedade compartilhada : a vida dos objetos está vinculada a um "contador de referência": funciona se a "propriedade" puder ser organizada hierarquicamente e falhar onde os laços de propriedade possam existir. Isto é o que std :: shared_ptr faz. E weak_ptr pode ser usado para quebrar o loop. Isso funciona a maior parte do tempo, mas falha no design grande, onde muitos designers trabalham em equipes diferentes e não há nenhuma razão clara (algo vindo de um requisito) sobre quem deve possuir o quê (o exemplo típico são cadeias de dois gostos): anterior devendo o próximo referente o anterior ou o próximo possuir o anterior referente ao seguinte? Em caso de necessidade um requisito as tho soluções são equivalentes, e em grande projeto arrisca misturá-las)
  2. Use uma pilha de coleta de lixo : você simplesmente não se importa com a vida útil. Você executa o coletor de tempos em tempos e o que é unreachabe é considerado "não mais necessário" e ... bem ... ahem ... destruído? finalizado? congeladas?. Há um número de coletores GC, mas eu nunca encontrei um que seja realmente C ++ ciente. A maioria deles libera a memória, não se importando com a destruição de objetos.
  3. Use um coletor de lixo ciente de C ++ , com uma interface de métodos padrão apropriada. Boa sorte para encontrá-lo.

Indo para a primeira solução para a última, a quantidade de estrutura de dados auxiliar necessária para gerenciar o aumento da vida útil do objeto, como o tempo gasto para organizá-lo e mantê-lo.

Coletor de lixo tem custo, shared_ptr tem menos, unique_ptr até menos, e objetos gerenciados de 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 o último recurso proposto por std:: . Mas pode ser a solução certa, se você estiver na situação que expliquei.

    
por 07.02.2012 / 09:55
fonte
9

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 interligado que precisa acontecer. No código multi-threaded em um sistema multi-core, a sincronização de memória não é insignificante. Dada a escolha, é melhor usar um valor de pilha ou um unique_ptr<> e referências de passagem ou ponteiros brutos.

    
por 04.02.2012 / 19:02
fonte
7

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 foi a última "escolha": dadas as condições de propriedade claras; unique_ptr, weak_ptr, shared_ptr e até ponteiros nus têm o seu lugar.

Uma coisa que todos concordaram é que somos (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 deveríamos esperar novas edições de alguns livros seminais de C ++, como C ++ efetivo (Meyers) e Padrões de Codificação em C ++ (Sutter & Alexandrescu), alguns anos depois, enquanto a indústria experiência e melhores práticas com o C ++ 11 panelas para fora.

    
por 04.02.2012 / 17:32
fonte
5

Eu acho que o que ele está chegando é que está se tornando comum que todos escrevam shared_ptr sempre que eles podem ter escrito um ponteiro padrão (como uma espécie de substituto global), e que ele está sendo usado como uma saída ao invés de projetar ou pelo menos planejando a criação e exclusão de objetos.

A outra coisa que as pessoas esquecem (além do gargalo de travamento / atualização / destravamento mencionado no material acima) é que o shared_ptr sozinho não resolve problemas de ciclo. Você ainda pode vazar recursos com shared_ptr:

Objeto A, contém um ponteiro compartilhado para outro Objeto A O objeto B cria A a1 e A a2 e atribui a1.otherA = a2; e a2.otherA = a1; Agora, os ponteiros compartilhados do objeto B usados para criar a1, a2 saem 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 para que suas contagens sejam sempre 1 e você vazou.

Esse é o exemplo simples, quando isso ocorre no código real, geralmente acontece de maneira complicada. Existe uma solução com o weak_ptr, mas muitas pessoas agora fazem o shared_ptr em todos os lugares e nem sequer sabem do problema de vazamento ou mesmo do weak_ptr.

Para finalizar: acho que os comentários mencionados pelo OP se resumem a isto:

Não importa em que idioma 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 sobre a criação de objetos, tempo de vida e destruição.

Editar: mesmo que isso signifique "desconhecido, preciso usar um shared_ptr", você ainda pensa nele e o faz intencionalmente.

    
por 05.02.2012 / 22:22
fonte
2

Responderei da minha experiência com Objective-C, uma linguagem na qual os objetos all são referenciados e alocados no heap. 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 a robustez do código e nenhum vazamento de memória. Também possibilitou que otimizações de compiladores inteligentes surgissem como o recente ARC (contagem automática de referência).

Meu ponto é 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.

    
por 10.08.2012 / 18:28
fonte
1

Vou tentar responder à pergunta:

How can we program without std::shared_ptr and still manage object lifetimes in safe way?

C ++ tem um grande número de maneiras diferentes de fazer memória, por exemplo:

  1. Use struct A { MyStruct s1,s2; }; em vez de shared_ptr no escopo da classe. Isso é apenas para programadores avançados, pois exige que você entenda como as dependências funcionam e exige 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 com tipos de c ++ embutidos nativos, mas seu uso com classes definidas pelo programador parece ser menos usado devido a esses problemas de dependência e ordem de classes. Esta solução também tem problemas com sizeof. Os programadores vêem problemas nisto como um requisito para usar declarações antecipadas ou #includes desnecessárias e, portanto, muitos programadores retornarão à solução inferior de ponteiros e depois a shared_ptr.
  2. Use MyClass &find_obj(int i); + clone () em vez de shared_ptr<MyClass> create_obj(int i); . Muitos programadores querem criar fábricas para criar novos objetos. shared_ptr é ideal para esse tipo de uso. O problema é que ele já assume soluções complexas de gerenciamento de memória usando alocação de heap / armazenamento livre, em vez de uma solução mais simples baseada em pilha ou 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 contido, 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 ele - construtores de cópia normais e construtor normal (de classe diferente) com parâmetro de referência ou clone () para objetos polimórficos podem manipulá-lo.
  3. Uso de referências em vez de ponteiros ou shared_ptrs. Cada classe c ++ possui construtores e cada membro de dados de referência precisa ser inicializado. Esse uso pode evitar muitos usos de ponteiros e shared_ptrs. Você só precisa escolher se a sua memória está dentro do objeto, ou fora dele, e escolha a solução struct ou solução de referência baseada na decisão. Os problemas com essa solução geralmente estão relacionados a evitar parâmetros de construtor, que é uma prática comum, mas problemática, e a entender mal como as interfaces para as classes devem ser projetadas.
por 04.02.2012 / 20:05
fonte