Puntero a implementación (PImpl)
"Puntero a implementación" o "pImpl (Pointer to implementation)" es una técnica de programación de C++ que elimina los detalles de implementación de una clase de su representación de objetos colocándolos en una clase separada, a la que se accede a través de un puntero opaco:
// -------------------- // interfaz (widget.h) struct widget { // miembros públicos private: struct impl; // declaración adelantada de la clase de implementación // Un ejemplo de implementación: mire más abajo para otras opciones de diseño std::experimental::propagate_const< // contenedor de reenvío constante de puntero std::unique_ptr< // opaco de propiedad única a la clase de impl>> pImpl; // implementación declarada por adelantado }; // --------------------------- // implementación (widget.cpp) struct widget::impl { // detalles de la implementación };
Esta técnica se usa para construir interfaces de biblioteca C++ con Interfaz Binaria de Aplicaciones (ABI) estable y para reducir las dependencias en tiempo de compilación.
Contenido |
[editar] Explicación
Debido a que los miembros de datos privados de una clase participan en la representación de su objeto, lo que afecta al tamaño y el diseño, y debido a que las funciones de los miembros privados de ua clase participan en la resolución de sobrecarga (que tiene lugar antes de la verificación del acceso de los miembros), cualquier cambio en esos detalles de implementación requiere de la recompilación de todos los usuarios de la clase.
pImpl elimina esta dependencia de compilación; los cambios en la implementación no provocan la recompilación. Por lo tanto, si una biblioteca utiliza pImpl en sus ABI, las versiones más nuevas de la biblioteca pueden cambiar la implementación si dejar de ser compatibles con ABI de versiones anteriores.
[editar] Alternativas
Las alternativas a pImpl son
- Implementación inline: los miembros privados y públicos son miembros de la misma clase.
- Clase abstracta pura (OOP factory): los usuarios obtienen un puntero único a una clase base ligera o abstracta, los detalles de implementación están en la clase derivada que sobreescribe sus funciones miembro virtuales.
[editar] Cortafuegos de compilación
En casos simples, tanto pImpl como el método factory elimina la dependencia en tiempo de compilación entre la implementación y los usuarios de la interfaz de clase. El método factory crea una dependencia oculta en vtable y, por lo tanto, reordenar, agregar o eliminar funciones miembros virtuales rompe la ABI. El enfoque pImpl no tiene dependencias ocultas; pero, si la clase de implementación es una especialización de plantilla de clase, se pierde el beneficio del cortafuegos de compilación: los usuarios de la interfaz deben observar la definición completa de la plantilla para crear una instancia de ña especialización correcta. Un enfoque de diseño común en este caso es refactorizar las implementación de una manera que evite la parametrización; este es otro caso de uso de las Directrices principales de C++:
- T.61 No sobreparametrizar miembros y
- T.84 Use una implementación principal sin plantilla para proporcionar una interfaz estable ABI.
Por ejemplo, la siguiente plantilla de clase no usa el tipo T
en su miembro privado ni en el cuerpo de push_back
:
template<class T> class ptr_vector { std::vector<void*> vp; public: void push_back(T* p) { vp.push_back(p); } };
Por lo tanto, los miembros privados se pueden transferir a la implementación tal cual, y push_back
se puede reenviar a una implementación que tampoco use T
en la interfaz:
// --------------------- // archivo cabecera (ptr_vector.hpp) #include <memory> class ptr_vector_base { struct impl; // no depende de T std::unique_ptr<impl> pImpl; protected: void push_back_fwd(void*); void print() const; // ... consulte la sección de implementación para funciones de miembros especiales public: ptr_vector_base(); ~ptr_vector_base(); }; template<class T> class ptr_vector : private ptr_vector_base { public: void push_back(T* p) { push_back_fwd(p); } void print() const { ptr_vector_base::print(); } }; // ----------------------- // archivo código fuente (ptr_vector.cpp) // #include "ptr_vector.hpp" #include <iostream> #include <vector> struct ptr_vector_base::impl { std::vector<void*> vp; void push_back(void* p) { vp.push_back(p); } void print() const { for (void const * const p: vp) std::cout << p << '\n'; } }; void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); } ptr_vector_base::ptr_vector_base() : pImpl{std::make_unique<impl>()} {} ptr_vector_base::~ptr_vector_base() {} void ptr_vector_base::print() const { pImpl->print(); } // --------------- // usuario (main.cpp) // #include "ptr_vector.hpp" int main() { int x{}, y{}, z{}; ptr_vector<int> v; v.push_back(&x); v.push_back(&y); v.push_back(&z); v.print(); }
Posible salida:
0x7ffd6200a42c 0x7ffd6200a430 0x7ffd6200a434
[editar] Sobrecarga en tiempo de ejecución
- Sobrecarga de acceso: En pImpl, cada llamada a una función miembro privada se dirige a través de un puntero. Cada acceso a un miembro público realizado por un miembro privado se dirige a través de otro puntero. Ambos direccionamientos indirectos cruzan los límites de las unidades de traducción, y por lo tanto, solo puede optimizarse mediante optimización en tiempo de enlazado. Tenga en cuenta que la OO factory requiere direccionamiento indirecto entre unidades de traducción para acceder tanto a los datos públicos como a los detalles de implementación, y ofrece menos oportunidades para el optimizador en tiempo de enlazado debido al despacho virtual.
- Sobrecarga de espacio: pImpl añade un puntero al componente público, y, si algún miembro privado necesita acceder a un miembro público, se añade otro puntero al componente de implementación o se pasa como parámetro para cada llamada al miembro privado que lo requiere. Si está soportado los asignadores personalizados con estado, la instancia del asignador también debe almacenarse.
- Sobrecarga de gestión de tiempo de vida: pImpl (así como OO factory) colocan el objeto de implementación en el heap, que impone una sobrecarga importante de tiempo de ejecución en la construcción y destrucción. Esto puede compensarse parcialmente con asignadores personalizados, puesto que el tamaño de asignación para pImpl (pero no para OO factory) se conoce en el momento de la compilación.
Por otro lado, las clase pImpl son fáciles de mover; refactorizar una clase grande como pImpl móvil puede mejorar el rendimiento de los algoritmos que manipulan contenedores que contienen dichos objetos, aunque pImpl móvil tiene una fuente adicional de sobrecarga en tiempo de ejecución: cualquier función miembro pública que esté permitida en un objeto movido y necesite acceso a una implementación privada incurre en una verificación de puntero nulo.
Esta sección está incompleta Razón: ¿Microbenchmark?) |
[editar] Sobrecarga de mantenimiento
El uso de pImpl requiere una unidad de traducción dedicada (una biblioteca con solo encabezado no puede usar pImpl), introduce una clase adicional, un conjunto de funciones de reenvío y, si se usan asignadores de memoria, expone los detalles de la implementación del uso de asignador en la interfaz pública.
Debido a que los miembros virtuales son parte del componente de interfaz de pImpl, simular un pImpl implica simular solo el componente de interfaz. Un pImpl comprobable esta diseñado generalmente para permitir un prueba completa a través dela interfaz disponible.
[editar] Implementación
Como el objeto del tipo de interfaz controla el tiempo de vida del objeto del tipo de implementación, el puntero a la implementación suele ser std::unique_ptr.
Debido a que std::unique_ptr requiere que el tipo apunto sea una tipo completo en cualquier contexto donde se instancia el eliminador, las funciones miembro especiales deben ser declaradas por el usuario y definidas fuera de línea, en el archivo de implementación, donde la clase de implementación es completa.
Debido a que cuando una función miembro constante llama a una función a través de un puntero miembro no constante, se llama a la sobrecarga no constante de la función de implementación, el puntero debe estar envuelto en std::experimental::propagate_const o equivalente.
Todos los miembros de datos privados y todas las funciones miembro privadas no virtuales se colocan en la clase de implementación. Todos los miembros públicos, protegidos y virtuales permanecen en la clase de interfaz (vea GOTW #100 para conocer alternativas).
Si alguno de los miembros privados necesita acceder a un miembro público o protegido, se puede pasar una referencia o puntero a la interfaz a la función privada como parámetro. Alternativamente, se puede mantener la referencia hacia atrás como parte de la clase de implementación.
Si se pretende admitir asignadores no predeterminados para la asignación del objeto de implementación, se puede usar cualquier patrón habitual conocido del asignador, incluyendo el parámetro de plantilla del asignador predeterminado en std::allocator y el argumento del constructor de tipo std::pmr::memory_resource*.
[editar] Notas
Esta sección está incompleta Razón: tenga en cuenta la conexión con el polimorfismo semántico de valores |
[editar] Example
Demostración de un pImpl con propagación constante, con referencia hacia atrás pasada como parámetro, sin conocimiento del asignador y habilitado para movimiento sin comprobaciones del tiempo de ejecución:
// ---------------------- // interfaz (widget.hpp) #include <experimental/propagate_const> #include <iostream> #include <memory> class widget { class impl; std::experimental::propagate_const<std::unique_ptr<impl>> pImpl; public: void draw() const; // API pública que se reenviará a la implementación void draw(); bool shown() const { return true; } // API pública a la que la implementación debe // llamar widget(); // incluso el constructor predeterminado debe definirse en el archivo // de implementación // Nota: llamar a draw() en el objeto construido por defecto es // comportamiento indefinido explicit widget(int); ~widget(); // definido en el archivo de implementación, donde impl es un tipo completo widget(widget&&); // definido en el archivo de implementación // Nota: llamar a draw() en un objeto movido es comportamiento // indefinido widget(const widget&) = delete; widget& operator=(widget&&); // definido en el archivo de implementación widget& operator=(const widget&) = delete; }; // --------------------------- // implementación (widget.cpp) // #include "widget.hpp" class widget::impl { int n; // dato privado public: void draw(const widget& w) const { if (w.shown()) // esta llamada a función de miembro público requiere la // referencia hacía atrás std::cout << "dibujando un widget contante " << n << '\n'; } void draw(const widget& w) { if (w.shown()) std::cout << "dibujando un widget no constante " << n << '\n'; } impl(int n) : n(n) {} }; void widget::draw() const { pImpl->draw(*this); } void widget::draw() { pImpl->draw(*this); } widget::widget() = default; widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {} widget::widget(widget&&) = default; widget::~widget() = default; widget& widget::operator=(widget&&) = default; // --------------- // usuario (main.cpp) // #include "widget.hpp" int main() { widget w(7); const widget w2(8); w.draw(); w2.draw(); }
Salida:
dibujando un widget no constante 7 dibujando un widget constante 8
Esta sección está incompleta Razón: describir otra alternativa — "Pimpl rápido". La principal diferencia es que la memoria para la implementación está reservada en un miembro de datos que es una array C opaco (dentro de la definición de la clase PImpl), mientras que en el archivo cpp esa memoria se asigna (a través de reinterpret_cast o placement-new ) a la estructura de la implementación. Este enfoque tiene sus propios pros y contras, en particular, una ventaja obvia es que no hay asignación de memoria adicional, con la condición de que inicialmente se haya reservado suficiente memoria en la fase de diseño de la clase PImpl. (Mientras que una de las desventajas es una menor facilidad de movimiento.) |
[editar] Enlaces externos
1. | GotW #28 : The Fast Pimpl Idiom. |
2. | GotW #100: Compilation Firewalls. |
3. | The Pimpl Pattern - what you should know. |