2011-02-07 13 views
21

queremos utilizar el idioma pimpl para ciertas partes de nuestro proyecto. Estas partes del proyecto también son partes en las que está prohibida la asignación de memoria dinámica y esta decisión no está bajo nuestro control.Idioma Pimpl sin utilizar la asignación de memoria dinámica

Entonces, lo que estoy preguntando es, ¿hay una manera limpia y agradable de implementar idioma pimpl sin asignación dinámica de memoria?

Editar
Aquí hay algunas otras limitaciones: plataforma integrada, estándar C++ 98, sin bibliotecas externas, sin plantillas.

+0

¿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. –

+12

Creo que el uso principal de pimpl está ocultando detalles de implementación, de ahí el nombre "puntero al idioma de implementación". – erelender

+2

@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

Respuesta

6

pimpl basa en los punteros y puede configurarlos en cualquier lugar donde se asignan los objetos. Esto también puede ser una tabla estática de objetos declarados en el archivo cpp. El punto principal de pimpl es mantener las interfaces estables y ocultar la implementación (y sus tipos usados).

+0

+1: buen punto! – neuro

+0

Creo que este es el mejor enfoque para nuestro caso, pero no creo que sea agradable y limpio como el pimpl estándar. – erelender

+2

En mi humilde opinión, el único inconveniente de este enfoque es que debe acordar un número máximo de objetos de ese tipo por adelantado/en tiempo de compilación. Para todos los demás aspectos en los que puedo pensar, se alcanzan los objetivos de pimpl. – jdehaan

3

Una forma sería tener una matriz char [] en su clase. Haga que sea lo suficientemente grande como para que su Impl encaje, y en su constructor, instale su Impl en su lugar en su matriz, con una ubicación nueva: new (&array[0]) Impl(...).

También debe asegurarse de no tener problemas de alineación, probablemente haciendo que su matriz char [] sea miembro de una unión. Este:

union { char array[xxx]; int i; double d; char *p; };

por ejemplo, se asegurará de que la alineación de array[0] será adecuado para un int, doble o un puntero.

+0

+1: Estaba escribiendo una publicación más larga, pero esto es básicamente eso.Podría escribir un segundo proyecto que tenga el tamaño de las clases impl y los instrumentos en las clases que lo contienen, de modo que no necesita realizar un seguimiento manual de cada cambio. –

+0

no estoy seguro de que los miembros de la unión sean suficientes para garantizar la alineación –

+1

Ese enfoque requiere que mantengamos el tamaño de la matriz de caracteres siempre que la implementación cambie (y puede cambiar con frecuencia en algunos lugares). Tampoco podemos hacerlo grande para el futuro porque la memoria es escasa. – erelender

3

Consulte The Fast Pimpl Idiom y The Joy of Pimpls sobre el uso de un asignador fijo junto con el idioma pimpl.

+2

Creo que escribir un asignador fijo omite todo el punto de "no usar memoria dinámica". Puede que no requiera asignación de memoria dinámica, pero requiere administración de memoria dinámica, que creo que no es diferente a anular nueva y eliminar globalmente. – erelender

4

Si puede usar el impulso, considere boost::optional<>. Esto evita el costo de la asignación dinámica, pero al mismo tiempo, su objeto no se construirá hasta que lo considere necesario.

+0

Lo sentimos, no podemos usar boost, o cualquier otro l biblioteca :( – erelender

+2

¿Por qué te disculpas, no puedes evitar las limitaciones artificiales? :) De todos modos, si quisieras, es bastante sencillo borrar el código de boost :: opcional, el bit más inteligente del código es la estructura 'aligned_storage' que declara una matriz de caracteres teniendo en cuenta la alineación, entonces es un simple colocación nueva para construir. – Nim

21

Advertencia: el código aquí solo muestra el aspecto de almacenamiento, es un esqueleto, no se ha tenido en cuenta ningún aspecto dinámico (construcción, copia, movimiento, destrucción).

Sugeriría un enfoque utilizando la nueva clase aligned_storage de C++ 0x, que es precisamente para tener almacenamiento en bruto.

// header 
class Foo 
{ 
public: 
private: 
    struct Impl; 

    Impl& impl() { return reinterpret_cast<Impl&>(_storage); } 
    Impl const& impl() const { return reinterpret_cast<Impl const&>(_storage); } 

    static const size_t StorageSize = XXX; 
    static const size_t StorageAlign = YYY; 

    std::aligned_storage<StorageSize, StorageAlign>::type _storage; 
}; 

En la fuente, a continuación, aplicar un cheque:

struct Foo::Impl { ... }; 

Foo::Foo() 
{ 
    // 10% tolerance margin 
    static_assert(sizeof(Impl) <= StorageSize && StorageSize <= sizeof(Impl) * 1.1, 
       "Foo::StorageSize need be changed"); 
    static_assert(StorageAlign == alignof(Impl), 
       "Foo::StorageAlign need be changed"); 
    /// anything 
} 

De esta manera, mientras que tendrá que cambiar la alineación de inmediato (si es necesario) el tamaño sólo cambiará si el objeto cambia demasiado.

Y, obviamente, ya que el cheque está en tiempo de compilación, simplemente no se lo puede perder :)

Si usted no tiene acceso a C++ 0x características, no son equivalentes en el espacio de nombres TR1 para aligned_storage y alignof y hay implementaciones de macros de static_assert.

+0

¿Por qué se necesita un 10% de margen de tolerancia aquí? – Gart

+2

@Gart: cualquier cambio en el tamaño de 'Foo' introduce una incompatibilidad binaria, que es lo que intentamos evitar aquí. Por lo tanto, necesita * StorageSize * para ser superior a 'sizeof (Impl)' * y * estable, por lo que es probable que pueda sobredimensionarlo ligeramente para poder agregar campos a 'Impl' más adelante. Sin embargo, puede sobrepasar demasiado y terminar con un objeto muy grande para ... nada, así que le sugiero que compruebe que tampoco termine con un objeto demasiado grande, utilizando este margen del 10%. –

+2

Necesitaba llamar 'new (& _storage) Impl();' en el constructor para obtener que los miembros de Pimpl inicien correctamente. – SurvivalMachine

1

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.

Cuestiones relacionadas