Reglas para administrar recuentos de referencias

El uso de un recuento de referencias para administrar la duración de un objeto permite a varios clientes obtener y liberar el acceso a un único objeto sin tener que coordinarse entre sí al administrar la duración del objeto. Siempre que el objeto del cliente se ajuste a ciertas reglas de uso, el objeto, en efecto, proporciona esta administración. Estas reglas especifican cómo administrar las referencias entre objetos. (COM no especifica implementaciones internas de objetos, aunque estas reglas son un punto de partida razonable para una directiva dentro de un objeto).

Conceptualmente, los punteros de interfaz se pueden considerar como residentes dentro de variables de puntero que incluyen todo el estado de cálculo interno que contiene un puntero de interfaz. Esto incluiría variables en ubicaciones de memoria, en registros de procesador internos y variables generadas por el programador y por el compilador. La asignación o inicialización de una variable de puntero implica la creación de una copia de un puntero ya existente. Donde antes había una copia del puntero en alguna variable (el valor usado en la asignación o inicialización), ahora hay dos. Una asignación a una variable de puntero destruye la copia del puntero actualmente en la variable, al igual que la destrucción de la propia variable. (Es decir, el ámbito en el que se encuentra la variable, como el marco de pila, se destruye).

Desde la perspectiva de un cliente COM, el recuento de referencias siempre se realiza para cada interfaz. Los clientes nunca deben suponer que un objeto usa el mismo contador para todas las interfaces.

El caso predeterminado es que se debe llamar a AddRef para cada nueva copia de un puntero de interfaz y a Release para cada destrucción de un puntero de interfaz, excepto cuando las reglas siguientes permitan lo contrario:

  • Parámetros de entrada y salida a funciones. El autor de la llamada debe llamar a AddRef en el parámetro porque se liberará (con una llamada a Release) en el código de implementación cuando el valor de salida se almacene sobre él.
  • Captura de una variable global. Al crear una copia local de un puntero de interfaz a partir de una copia existente del puntero en una variable global, debe llamar a AddRef en la copia local porque otra función podría destruir la copia en la variable global mientras la copia local sigue siendo válida.
  • Nuevos punteros sintetizados que aparecen "de la nada". Una función que sintetiza un puntero de interfaz mediante conocimientos internos especiales en lugar de obtenerlo de otro origen debe llamar a AddRef inicialmente en el puntero recién sintetizado. Entre los ejemplos importantes de estas rutinas se incluyen las de creación de instancias, implementaciones de QueryInterface, etc.
  • Recuperación de una copia de un puntero almacenado internamente. Cuando una función recupera una copia de un puntero que almacena el objeto al que se llama, el código del objeto debe llamar a AddRef en el puntero antes de que la función devuelva valores. Una vez recuperado el puntero, el objeto de origen no tiene ninguna otra manera de determinar cómo se relaciona su duración con la de la copia almacenada internamente del puntero.

Las únicas excepciones al caso predeterminado requieren que el código de administración conozca las relaciones de las duraciones de dos o más copias de un puntero a la misma interfaz en un objeto y simplemente se aseguren de que el objeto no se destruye al permitir que su recuento de referencias vaya a cero. En general, hay dos casos, como se indica a continuación:

  • Cuando ya existe una copia de un puntero, se crea posteriormente una segunda y, después, se destruye mientras la primera copia sigue existiendo, se pueden omitir las llamadas a AddRef y Release para la segunda copia.
  • Cuando existe una copia de un puntero, se crea una segunda y, después, se destruye la primera antes de la segunda, se pueden omitir las llamadas a AddRef para la segunda copia y a Release para la primera.

A continuación se muestran ejemplos específicos de estas situaciones; los dos primeros ejemplos son especialmente comunes:

  • Parámetros de entrada a funciones. La duración de la copia de un puntero de interfaz que se pasa como parámetro a una función está anidada en la del puntero usado para inicializar el valor, por lo que no es necesario contar con una referencia independiente en el parámetro.
  • Parámetros de salida desde funciones, incluidos los valores devueltos. Para establecer el parámetro de salida, la función debe tener una copia estable del puntero de interfaz. En la devolución, el llamador es responsable de lanzar el puntero. Por lo tanto, el parámetro de salida no necesita un recuento de referencias independiente.
  • Variables locales. Una implementación de método tiene el control de las duraciones de cada una de las variables de puntero asignadas en el marco de pila y puede usarlo para determinar cómo omitir pares de versión de AddRef/Release redundantes.
  • Backpointers. Algunas estructuras de datos contienen dos objetos, cada uno con un puntero en el otro. Si se sabe que la duración del primer objeto contiene la duración del segundo, no es necesario tener un recuento de referencias en el puntero del segundo objeto al primer objeto. A menudo, evitar este ciclo es importante para mantener el comportamiento de desasignación adecuado. Pero los punteros sin contar deben usarse con extrema precaución porque la parte del sistema operativo que controla el procesamiento remoto no tiene forma de conocer esta relación. Por lo tanto, en casi todos los casos, que el backpointer vea un segundo objeto "friend" del primer puntero (evitando así la circularidad) es la solución preferida. Por ejemplo, la arquitectura de objetos conectables de COM usa este enfoque.

Al implementar o usar objetos con recuento de referencias, puede resultar útil aplicar recuentos de referencia artificiales, lo que garantiza la estabilidad del objeto durante el procesamiento de una función. Al implementar un método de una interfaz, puede llamar a funciones que tienen la posibilidad de disminuir el recuento de referencias en un objeto, lo que provoca un lanzamiento prematuro del objeto y un error de la implementación. Una forma sólida de evitar esto es insertar una llamada a AddRef al principio de la implementación del método y emparejarla con una llamada a Release justo antes de que el método devuelva valores.

En algunas situaciones, los valores devueltos de AddRef y Release pueden ser inestables y no se debe basar en ellos; solo deben usarse con fines de depuración o diagnóstico.

Administración de la duración de los objetos mediante el recuento de referencias