Como funciona o weak_ptr?

Eu entendo como usar weak_ptr e shared_ptr . Eu entendo como funciona o shared_ptr , contando o número de referências em seu object. Como funciona o weak_ptr ? Eu tentei ler o código fonte do boost e não estou familiarizado o suficiente com o boost para entender todas as coisas que ele usa.

Obrigado.

shared_ptr usa um object “contador” extra (também conhecido como “contagem compartilhada” ou “bloco de controle”) para armazenar a contagem de referência. (BTW: esse object “contador” também armazena o deleter.)

Cada shared_ptr e weak_ptr contém um ponteiro para o pointee real e um segundo ponteiro para o object “counter”.

Para implementar weak_ptr , o object “contador” armazena dois contadores diferentes:

  • A “contagem de uso” é o número de instâncias de shared_ptr apontando para o object.
  • A “contagem fraca” é o número de instâncias de weak_ptr apontando para o object, mais uma se a “contagem de uso” ainda for> 0.

O pointee é excluído quando a “contagem de uso” atingir zero.

O object auxiliar “contador” é excluído quando a “contagem fraca” atingir zero (o que significa que a “contagem de uso” também deve ser zero, veja acima).

Quando você tenta obter um shared_ptr de um weak_ptr , a biblioteca verifica atomicamente a “contagem de uso” e, se for> 0, o incrementa. Se isso for bem sucedido, obtenha o seu shared_ptr . Se a “contagem de uso” já foi zero, você recebe uma instância shared_ptr vazia.


EDIT : Agora, por que eles adicionam um para a contagem fraca em vez de apenas liberar o object “contador” quando ambas as contagens cair para zero? Boa pergunta.

A alternativa seria excluir o object “contador” quando a contagem de uso e a contagem fraca caem para zero. Aqui está o primeiro motivo: não é possível verificar dois contadores (tamanho de ponteiro) atomicamente em todas as plataformas e, mesmo onde está, é mais complicado do que verificar apenas um contador.

Outra razão é que o deleter deve permanecer válido até terminar a execução. Como o deleter é armazenado no object “contador”, isso significa que o object “contador” deve permanecer válido. Considere o que poderia acontecer se houvesse um shared_ptr e um weak_ptr para algum object, e eles fossem redefinidos ao mesmo tempo em encadeamentos simultâneos. Vamos dizer que o shared_ptr vem em primeiro lugar. Diminui o “use count” para zero e começa a executar o deleter. Agora, o weak_ptr diminui a “contagem fraca” para zero e descobre que a “contagem de uso” também é zero. Assim, elimina o object “contador” e, com ele, o deléter. Enquanto o deleter ainda está em execução.

É claro que haveria maneiras diferentes de garantir que o object “contador” permaneça vivo, mas acho que aumentar a “contagem fraca” em um é uma solução muito elegante e intuitiva. A “contagem fraca” torna-se a contagem de referência para o object “contador”. E como shared_ptr s referenciam o object contador também, eles também precisam incrementar a “contagem fraca”.

Uma solução provavelmente ainda mais intuitiva seria incrementar a “contagem fraca” para cada shared_ptr , uma vez que cada propriedade shared_ptr contém uma referência ao object “counter”.

Adicionar uma para todas as instâncias shared_ptr é apenas uma otimização (salva um incremento / decremento atômico ao copiar / atribuir instâncias shared_ptr ).

Basicamente, um “weak_ptr” é um ponteiro “T *” comum que permite recuperar uma referência forte, ou seja, “shared_ptr”, posteriormente no código.

Assim como um T * comum, o weak_ptr não faz nenhuma contagem de referência. Internamente, para suportar a contagem de referência em um tipo arbitrário T, o STL (ou qualquer outra biblioteca que implemente esse tipo de lógica) cria um object wrapper que chamaremos de “Anchor”. “Âncora” existe somente para implementar a contagem de referência e o comportamento “quando a contagem é zero, a exclusão de chamada” é necessária.

Em uma referência forte, o shared_ptr implementa sua cópia, operador =, construtor, destrutor e outras APIs pertinentes para atualizar a contagem de referência de “Âncora”. É assim que um shared_ptr garante que seu “T” viva exatamente enquanto alguém o estiver usando. Em um “weak_ptr”, essas mesmas APIs apenas copiam o Anchor ptr atual. Eles NÃO atualizam as contagens de referência.

É por isso que as APIs mais importantes de “weak_ptr” são “expiradas” e as de “bloqueio” com nome insatisfatório. “Expirado” informa se o object subjacente ainda está por perto, ou seja, “Ele já foi excluído porque todas as referências fortes ficaram fora do escopo?”. “Lock” irá (se puder) converter o weak_ptr para uma referência forte shared_ptr, restaurando a contagem de referência.

BTW, “lock” é um nome terrível para essa API. Você não está (apenas) invocando um mutex, você está criando uma forte referência de um fraco, com aquele “Anchor” agindo. A maior falha em ambos os modelos é que eles não implementaram operator->, então para fazer qualquer coisa com o seu object você tem que recuperar o raw “T *”. Eles fizeram isso principalmente para suportar coisas como “shared_ptr”, porque tipos primitivos não suportam o operador “->”.