El patrón de diseño PImpl (Pointer to Implementation), también conocido como puntero opaco, surgió como un medio para separar la interfaz de la implementación de una clase en C++. Esto es especialmente importante para garantizar la compatibilidad de los interfaces binarios (ABI), acelerar la compilación y ocultar los detalles de implementación del usuario de la clase.
Historia del asunto.
En un gran número de proyectos de C++, a menudo es necesario modificar las implementaciones de las clases sin cambiar la interfaz pública y sin recompilar los clientes de esas clases. El problema es que cualquier cambio en los archivos de encabezado requiere la reconstrucción de todos los módulos dependientes, lo que puede ser extremadamente costoso en grandes bases de código. PImpl permite minimizar la recompilación y proporciona una mejor encapsulación.
Problema.
La forma estándar de definir una clase con miembros privados en un archivo de encabezado requiere conocer todos esos miembros en el momento de la compilación. Al ampliar o modificar estos miembros, es necesario volver a compilar todos los archivos que incluyen este encabezado. Además, esto revela los detalles de implementación/estructura del cliente, lo que puede afectar negativamente la seguridad y la integridad arquitectónica.
Solución.
PImpl implementa el ocultamiento de la implementación mediante el uso de un puntero a una estructura de implementación (Impl struct/class) declarada por adelantado, definida en cpp. Esto permite cambiar la implementación sin afectar a la interfaz.
Ejemplo de código:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // puntero opaco }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secreto interior Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Características clave:
¿Se puede usar std::unique_ptr en lugar de un puntero crudo en PImpl?
Sí, un enfoque moderno y seguro es utilizar std::unique_ptr (o std::shared_ptr, si se requiere compartir la propiedad). Esto permite gestionar correctamente la memoria y no escribir explícitamente un destructor/un operador de copia para el puntero crudo:
private: std::unique_ptr<Impl> pimpl;
¿Se puede hacer que una clase con PImpl sea movible pero no copiable?
Sí, si se proporciona un constructor/operator de movimiento, pero se elimina el de copia. Por ejemplo:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
¿Hay una sobrecarga de rendimiento al usar PImpl?
Sí, debido a la desreferenciación del puntero y la asignación dinámica adicional de memoria (asignación en el heap). Para estructuras de rendimiento crítico, esto puede ser una desventaja significativa.
Una gran empresa implementó PImpl para todas las clases de manera indiscriminada, incluso para estructuras de datos simples, lo que llevó a una desaceleración considerable de las operaciones simples debido a la constante desreferenciación de punteros.
Ventajas:
Desventajas:
En un proyecto con una biblioteca de interfaz de usuario de larga duración, se aplicó PImpl solo para widgets complejos con un interior que cambia con frecuencia, manteniendo un ABI estable para clientes externos.
Ventajas:
Desventajas: