El objetivo de usar pimpl es ocultar la implementación de su objeto. Esto incluye el tamaño del verdadero objeto de implementación.Sin embargo, esto también hace que sea difícil evitar la asignación dinámica: para reservar suficiente espacio de pila para el objeto, necesita saber qué tan grande es el objeto.
La solución típica es utilizar la asignación dinámica y pasar la responsabilidad de asignar suficiente espacio a la implementación (oculta). Sin embargo, esto no es posible en su caso, entonces necesitaremos otra opción.
Una de estas opciones es usar alloca()
. Esta función poco conocida asigna memoria en la pila; la memoria se liberará automáticamente cuando la función salga de su alcance. Esto no es portable C++, sin embargo muchas implementaciones de C++ lo admiten (o una variación de esta idea).
Tenga en cuenta que debe asignar sus objetos pimpl'd utilizando una macro; alloca()
debe invocarse para obtener la memoria necesaria directamente desde la función propietaria. Ejemplo:
// Foo.h
class Foo {
void *pImpl;
public:
void bar();
static const size_t implsz_;
Foo(void *);
~Foo();
};
#define DECLARE_FOO(name) \
Foo name(alloca(Foo::implsz_));
// Foo.cpp
class FooImpl {
void bar() {
std::cout << "Bar!\n";
}
};
Foo::Foo(void *pImpl) {
this->pImpl = pImpl;
new(this->pImpl) FooImpl;
}
Foo::~Foo() {
((FooImpl*)pImpl)->~FooImpl();
}
void Foo::Bar() {
((FooImpl*)pImpl)->Bar();
}
// Baz.cpp
void callFoo() {
DECLARE_FOO(x);
x.bar();
}
Esto, como se puede ver, la sintaxis hace bastante incómodo, pero sí lograr un análogo pimpl.
Si puede codificar el tamaño del objeto en la cabecera, también existe la opción de usar una matriz de caracteres:
class Foo {
private:
enum { IMPL_SIZE = 123; };
union {
char implbuf[IMPL_SIZE];
double aligndummy; // make this the type with strictest alignment on your platform
} impl;
// ...
}
Esto es menos puro que el enfoque anterior, ya que debe cambiar los encabezados siempre que sea el tamaño de implementación cambia. Sin embargo, le permite usar la sintaxis normal para la inicialización.
También podría implementar una pila paralela, es decir, una pila secundaria separada de la pila normal de C++, específicamente para contener objetos pImpl'd. Esto requiere una administración muy cuidadosa, pero, debidamente ajustado, debería funcionar. Este tipo de se encuentra en la zona gris entre la asignación dinámica y la estática.
// One instance per thread; TLS is left as an exercise for the reader
class ShadowStack {
char stack[4096];
ssize_t ptr;
public:
ShadowStack() {
ptr = sizeof(stack);
}
~ShadowStack() {
assert(ptr == sizeof(stack));
}
void *alloc(size_t sz) {
if (sz % 8) // replace 8 with max alignment for your platform
sz += 8 - (sz % 8);
if (ptr < sz) return NULL;
ptr -= sz;
return &stack[ptr];
}
void free(void *p, size_t sz) {
assert(p == stack[ptr]);
ptr += sz;
assert(ptr < sizeof(stack));
}
};
ShadowStack theStack;
Foo::Foo(ShadowStack *ss = NULL) {
this->ss = ss;
if (ss)
pImpl = ss->alloc(sizeof(FooImpl));
else
pImpl = new FooImpl();
}
Foo::~Foo() {
if (ss)
ss->free(pImpl, sizeof(FooImpl));
else
delete ss;
}
void callFoo() {
Foo x(&theStack);
x.Foo();
}
Con este enfoque es fundamental para asegurarse de que usted no usa la pila sombra para los objetos donde el objeto envoltorio está en el montón; esto violaría la suposición de que los objetos siempre se destruyen en el orden inverso de la creación.
¿Cuál es el punto de pimpl sin asignación dinámica? El uso principal de pimpl es hacer que la vida de los objetos dinámicos sea manejable. Si no tiene problemas de administración de por vida, simplemente pase la referencia al objeto de ámbito estático/de pila directamente. –
Creo que el uso principal de pimpl está ocultando detalles de implementación, de ahí el nombre "puntero al idioma de implementación". – erelender
@Chris: no necesitamos pimpl para administrar la vida de los objetos. Solo use un puntero inteligente (o escriba el objeto para seguir el modismo RAII en primer lugar). pimpl se trata de ocultar los aspectos internos de una clase. – jalf