Regras para gerenciar contagens de referência
O uso de uma contagem de referência para gerenciar o tempo de vida de um objeto permite que vários clientes obtenham e liberem acesso a um único objeto sem precisar se coordenar entre si no gerenciamento do tempo de vida do objeto. Desde que o objeto cliente esteja em conformidade com certas regras de uso, o objeto, de fato, fornece esse gerenciamento. Essas regras especificam como gerenciar referências entre objetos. (COM não especifica implementações internas de objetos, embora essas regras sejam um ponto de partida razoável para uma política dentro de um objeto.)
Conceitualmente, os ponteiros de interface podem ser considerados como residindo dentro de variáveis de ponteiro que incluem todo o estado de computação interno que contém um ponteiro de interface. Isso incluiria variáveis em locais de memória, em registradores internos do processador e variáveis geradas pelo programador e pelo compilador. A atribuição ou inicialização de uma variável de ponteiro envolve a criação de uma nova cópia de um ponteiro já existente. Onde havia uma cópia do ponteiro em alguma variável (o valor usado na atribuição/inicialização), agora há duas. Uma atribuição a uma variável de ponteiro destrói a cópia de ponteiro presente na variável, assim como a destruição da própria variável. (Ou seja, o escopo no qual a variável é encontrada, como o quadro de pilha, é destruído.)
Do ponto de vista de um cliente COM, a contagem de referências é sempre feita para cada interface. Os clientes nunca devem assumir que um objeto usa o mesmo contador para todas as interfaces.
O caso padrão é que AddRef deve ser chamado para cada nova cópia de um ponteiro de interface e Release deve ser chamado para cada destruição de um ponteiro de interface, exceto quando as seguintes regras permitem o contrário:
- Parâmetros de entrada e saída para funções. O chamador deve chamar AddRef no parâmetro porque ele será liberado (com uma chamada para Release) no código de implementação quando o valor de saída for armazenado sobre ele.
- Buscando uma variável global. Ao criar uma cópia local de um ponteiro de interface a partir de uma cópia existente do ponteiro em uma variável global, você deve chamar AddRef na cópia local porque outra função pode destruir a cópia na variável global enquanto a cópia local ainda é válida.
- Novos ponteiros sintetizados a partir do "nada". Uma função que sintetiza um ponteiro de interface usando conhecimento interno especial em vez de obtê-lo de alguma outra fonte deve chamar AddRef inicialmente no ponteiro recém-sintetizado. Exemplos importantes de tais rotinas incluem rotinas de criação de instâncias, implementações de QueryInterface e assim por diante.
- Recuperando uma cópia de um ponteiro armazenado internamente. Quando uma função recupera uma cópia de um ponteiro que é armazenado internamente pelo objeto chamado, o código desse objeto deve chamar AddRef no ponteiro antes que a função retorne. Depois que o ponteiro é recuperado, o objeto de origem não tem outra maneira de determinar como seu tempo de vida se relaciona com o da cópia armazenada internamente do ponteiro.
As únicas exceções ao caso padrão exigem que o código de gerenciamento conheça as relações dos tempos de vida de duas ou mais cópias de um ponteiro para a mesma interface em um objeto e simplesmente certifique-se de que o objeto não seja destruído, permitindo que sua contagem de referência vá a zero. Geralmente, há dois casos, como segue:
- Quando uma cópia de um ponteiro já existe e uma segunda é criada posteriormente e, em seguida, é destruída enquanto a primeira cópia ainda existe, as chamadas para AddRef e Release para a segunda cópia podem ser omitidas.
- Quando uma cópia de um ponteiro existe e uma segunda é criada e, em seguida, a primeira é destruída antes da segunda, as chamadas para AddRef para a segunda cópia e para Release para a primeira cópia podem ser omitidas.
A seguir estão exemplos específicos dessas situações, sendo as duas primeiras especialmente comuns:
- Em parâmetros para funções. O tempo de vida da cópia de um ponteiro de interface passado como um parâmetro para uma função é aninhado no do ponteiro usado para inicializar o valor, portanto, não há necessidade de uma contagem de referência separada no parâmetro.
- Parâmetros de saída de funções, incluindo valores de retorno. Para definir o parâmetro out, a função deve ter uma cópia estável do ponteiro da interface. No retorno, o chamador é responsável por liberar o ponteiro. Portanto, o parâmetro out não precisa de uma contagem de referência separada.
- Variáveis locais. Uma implementação de método tem controle dos tempos de vida de cada uma das variáveis de ponteiro alocadas no quadro de pilha e pode usar isso para determinar como omitir pares redundantes AddRef/Release.
- Backpointers. Algumas estruturas de dados contêm dois objetos, cada um com um ponteiro para o outro. Se o tempo de vida do primeiro objeto é conhecido por conter o tempo de vida do segundo, não é necessário ter uma contagem de referência no ponteiro do segundo objeto para o primeiro objeto. Muitas vezes, evitar esse ciclo é importante para manter o comportamento adequado de desalocação. No entanto, ponteiros não contados devem ser usados com extrema cautela porque a parte do sistema operacional que lida com o processamento remoto não tem como saber sobre essa relação. Portanto, em quase todos os casos, fazer com que o backpointer veja um segundo objeto "amigo" do primeiro ponteiro (evitando assim a circularidade) é a solução preferida. A arquitetura de objetos conectáveis da COM, por exemplo, usa essa abordagem.
Ao implementar ou usar objetos contados por referência, pode ser útil aplicar contagens de referência artificiais, que garantem a estabilidade do objeto durante o processamento de uma função. Ao implementar um método de uma interface, você pode chamar funções que têm uma chance de diminuir sua contagem de referência para um objeto, causando uma liberação prematura do objeto e falha da implementação. Uma maneira robusta de evitar isso é inserir uma chamada para AddRef no início da implementação do método e emparelhá-la com uma chamada para Release pouco antes do método retornar.
Em algumas situações, os valores de retorno de AddRef e Release podem ser instáveis e não devem ser confiáveis, eles devem ser usados apenas para fins de depuração ou diagnóstico.
Tópicos relacionados