Espacios de nombres
Variantes
Acciones

Puntero a implementación (PImpl)

De cppreference.com
< cpp‎ | language
 
 
Lenguaje C++
Temas generales
Control de flujo
Instrucciones de ejecución condicionales
Instrucciones de iteración (bucles)
Declaraciones de salto
Funciones
Declaración de funciones
Declaración de funciones lambda
Especificador inline
Especificación de excepciones (hasta C++20)
Especificador noexcept (C++11)
Excepciones
Espacios de nombres
Tipos
Especificadores
decltype (C++11)
auto (C++11)
alignas (C++11)
Especificadores de duración de almacenamiento
Inicialización
Expresiones
Representaciones alternas
Literales
Booleanos - Enteros - De punto flotante
De carácter - De cadena - nullptr (C++11)
Definidos por el usuario (C++11)
Utilidades
Atributos (C++11)
Tipos
Declaración de typedef
Declaración de alias de tipo (C++11)
Conversiones
Conversiones implícitas - Conversiones explícitas
static_cast - dynamic_cast
const_cast - reinterpret_cast
Asignación de memoria
Clases
Propiedades de funciones específicas de la clase
Funciones miembro especiales
Plantillas
Misceláneos
 

"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++:

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.

[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

[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

[editar] Enlaces externos

1.  GotW #28 : The Fast Pimpl Idiom.
2.  GotW #100: Compilation Firewalls.
3.  The Pimpl Pattern - what you should know.