Buen patrón de diseño para un contenedor c ++ alrededor del objeto ac

8

He escrito un contenedor de C ++ extensible alrededor de una biblioteca c muy difícil de usar pero también muy útil. El objetivo es tener la conveniencia de c ++ para asignar el objeto, exponer sus propiedades, desasignar el objeto, copiar semántica, etc.

El problema es este: a veces la biblioteca c quiere el objeto subyacente (un puntero al objeto), y el destructor de clase no debe destruir la memoria subyacente. Si bien la mayoría de las veces, el destructor debe desasignar el objeto subyacente. He experimentado con la configuración de un bool hasOwnershipindicador en la clase para que el destructor, el operador de asignación, etc. sepan si debe o no liberar la memoria subyacente. Sin embargo, esto es engorroso para el usuario y, a veces, no hay forma de saber cuándo otro proceso usará esa memoria.

Actualmente, lo tengo configurado donde cuando la asignación proviene de un puntero del mismo tipo que el tipo subyacente, entonces configuro el indicador hasOwnership. Hago lo mismo cuando se llama al constructor sobrecargado usando el puntero de la biblioteca c. Sin embargo, esto todavía no maneja el caso cuando el usuario ha creado el objeto y lo ha pasado a una de mis funciones que llama a c_api y la biblioteca almacena el puntero para su uso posterior. Si eliminaran su objeto, sin duda causaría un defecto en la biblioteca c.

¿Existe un patrón de diseño que simplifique este proceso? ¿Quizás algún tipo de recuento de referencias?

Jonathan Henson
fuente
parece no haber pensado en la seguridad del hilo de estos objetos ...
Ratchet Freak
@ratchetfreak Todo lo que hace el contenedor es llamar al descriptor de acceso / mutadores, asegurar una buena semántica de copia, manejar la memoria, copiar listas, etc. En realidad, no toco directamente los datos. La biblioteca c ya es segura para subprocesos. Además, mis usuarios solo tendrán acceso a mis contenedores sincrónicamente con el sistema de devoluciones de llamada, etc. Cuando es necesario, tengo bloqueos en lecturas / escrituras en cualquier dato.
Jonathan Henson el
2
unique_ptren la mayoría de los casos, ya puede manejar este tipo de recursos, por lo que no necesita implementar la administración de recursos usted mismo. unique_ptrutiliza el releasemétodo para renunciar a la propiedad del objeto almacenado.
Philipp

Respuestas:

4

Si la responsabilidad de limpiar las cosas asignadas dinámicamente cambia entre su clase de contenedor y la biblioteca C en función de cómo se usan las cosas, está tratando con una biblioteca C mal diseñada o está tratando de hacer demasiado en su clase de contenedor.

En el primer caso, todo lo que puede hacer es realizar un seguimiento de quién es responsable de la limpieza y esperar que no se cometan errores (ni usted ni los encargados del mantenimiento de la biblioteca C).

En el segundo caso, debe repensar el diseño de su envoltorio. ¿Todas las funciones pertenecen juntas en la misma clase, o se pueden dividir en varias clases? Quizás la biblioteca C usa algo similar al patrón de diseño de Fachada y debería mantener una estructura similar en su contenedor C ++.

En cualquier caso, incluso si la biblioteca C es responsable de limpiar algunas cosas, no hay nada de malo en mantener una referencia / puntero a esas cosas. Solo necesita recordar que no es responsable de limpiar a qué se refiere el puntero.

Bart van Ingen Schenau
fuente
¿Qué pasa si me aseguro de que se haya hecho un clon del objeto subyacente cuando se pasa a la biblioteca? Entonces puedo liberar mi memoria como quiera y no afectará nada. Sin embargo, no recibiré cambios en el objeto en mi clase.
Jonathan Henson el
Y sí, se usaron algunas prácticas muy malas en esta lib. Por ejemplo: en lugar de hacer que el usuario pase un const char * a una función que establece una cadena en una estructura y luego haga una copia para almacenar en la estructura, espera que el usuario asigne la cadena sobre la marcha y pase un char * que simplemente se almacena sin hacer una copia. Esa es una de las principales razones por las que hice un contenedor: para garantizar que se siguiera la semántica de copia adecuada.
Jonathan Henson el
@JonathanHenson: Permitir que el cliente asigne algo y transfiera su propiedad / responsabilidad a la biblioteca es una técnica de optimización común para evitar el exceso de copia y es completamente válido para las bibliotecas C (siempre y cuando se haga de manera consistente). Pero de hecho significa que, cuando se usa desde C ++, el usuario puede necesitar hacer copias adicionales para cumplir con los requisitos de la interfaz de la biblioteca C.
Bart van Ingen Schenau
3

A menudo puedes usar un patrón como este:

class C {
public:
  void foo() {
    underlying_foo(handle.get());
  }

  void bar() {
    // transfers ownership
    underlying_bar(handle.release());
  }

  // use default copy/move constructor and assignment operator

private:
  struct deleter {
    void operator()(T* ptr) {
      deleter_fn(ptr);
    }
  };
  std::unique_ptr<T, deleter> handle;
};

Al usarlo releasepuede transferir explícitamente la propiedad. Sin embargo, esto es confuso y debe evitarlo si es posible.

La mayoría de las bibliotecas de C tienen un ciclo de vida de objeto similar a C ++ (asignación de objetos, accesores, destrucción) que se asigna muy bien al patrón de C ++ sin transferencia de propiedad.

Si los usuarios necesitan propiedad compartida, deberían usarla shared_ptrcon sus clases. No intentes implementar ninguna propiedad compartiéndote.


Actualización: si desea que la transferencia de propiedad sea más explícita, puede usar un calificador de referencia:

void bar() && { ... }

Luego, los usuarios deben invocar barvalores como este:

C o;
std::move(o).bar();  // transfer of ownership is explicit at call site
Philipp
fuente
@Phillip, parece exactamente lo que necesito. Intentaré esto y volveré contigo.
Jonathan Henson el
Una pregunta. ¿Qué es exactamente la propiedad en este caso? ¿Significa el privilegio de acceder / modificar el objeto, o simplemente la capacidad de construir y destruir el objeto?
Jonathan Henson el
Básicamente, una vez que paso la memoria subyacente a la c-lib, se supone que nunca debo hacer una llamada gratuita a la memoria. Sin embargo, puedo y con frecuencia tendré que acceder a los datos del objeto.
Jonathan Henson el
2
La propiedad es el derecho a destruir el objeto.
DeadMG
@JonathanHenson: ese es un caso interesante que no está cubierto en este momento en este idioma: una vez que ha lanzado un unique_ptr, ya no almacena el puntero que tenía. Modelar la transferencia de propiedad y, al mismo tiempo, permitir el acceso a recursos desposeídos parece ser bastante confuso para los usuarios. ¿Cómo se controla cuando la biblioteca C libera los objetos que ahora posee?
Philipp
0

Hay una respuesta directa a su problema, punteros inteligentes. Al usar un puntero inteligente para mantener la memoria de la biblioteca C y agregar una referencia cuando el puntero también está en la biblioteca (y soltar la referencia cuando la biblioteca C vuelve), liberará automáticamente la memoria cuando el recuento de referencia caiga a cero (y sólo entonces)

Michael Shaw
fuente
Esto no soluciona el problema, a menos que no entienda mal los punteros inteligentes. El problema es que a veces, mi destructor debería limpiar la memoria, otras veces la biblioteca c limpia la memoria. Estas estructuras tienen init especial, funciones libres. La biblioteca puede llamar gratis por sí sola, antes de que el recuento de referencias del puntero inteligente llegue a 0. Necesito básicamente decidir la propiedad y dejarlo ir. No necesito un puntero inteligente para saber cuándo limpiar la memoria, el problema es que a veces, no soy el responsable de limpiarlo y no necesito liberarlo en el destructor.
Jonathan Henson el
Sí, estoy de acuerdo, entendí mal el problema. Solo una observación: ¿comparten las diferentes bibliotecas la misma tabla de asignación de memoria? Mis cosas intensas de C / C ++ fueron hace 10 años, y en Visual Studio en ese entonces, asignar memoria en una biblioteca y liberarla en otra biblioteca sería una pérdida de memoria, a menos que haya cambiado algunas opciones del compilador para AMBAS bibliotecas. La consecuencia de hacer esto sería la contención de la gestión de memoria en aplicaciones altamente subprocesadas. Sin embargo, es bastante probable que sea información obsoleta.
Michael Shaw
no si todo está cargado en el mismo espacio de direcciones, y así es. De todos modos, estoy volviendo a compilar la c-lib en el mismo ensamblado que mi contenedor.
Jonathan Henson el
0

Si la biblioteca puede liberar cosas internamente, y los escenarios donde esto puede suceder están bien documentados, entonces todo lo que puede hacer es establecer una bandera como ya lo está haciendo.

James
fuente
básicamente, una vez que paso la memoria subyacente a la c-lib, se supone que nunca debo hacer una llamada gratuita a la memoria. Sin embargo, puedo y con frecuencia tendré que acceder a los datos del objeto.
Jonathan Henson el