Идиома Pimpl без использования динамического распределения памяти

мы хотим использовать идиому pimpl для некоторых частей нашего проекта. Эти части проекта также являются частями, где динамическое распределение памяти запрещено, и это решение не находится под нашим контролем.

Так что я спрашиваю, есть ли чистый и приятный способ реализации идиомы pimpl без динамического выделения памяти?

Изменить
Вот некоторые другие ограничения: встроенная платформа, стандартный С++ 98, внешние библиотеки и шаблоны.

Ответ 1

Предупреждение: здесь представлен только код хранения, это скелет, динамический аспект (конструкция, копирование, перемещение, уничтожение) был учтен.

Я бы предложил подход с использованием нового класса С++ 0x aligned_storage, который предназначен для хранения необработанных данных.

// 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;
};

В исходном коде затем выполняется проверка:

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
}

Таким образом, если вам нужно немедленно изменить выравнивание (если необходимо), размер изменится только в том случае, если объект слишком сильно изменился.

И, очевидно, поскольку проверка выполняется во время компиляции, вы просто не можете пропустить это:)

Если у вас нет доступа к функциям С++ 0x, существуют эквиваленты в пространстве имен TR1 для aligned_storage и alignof и существуют реализации макросов static_assert.

Ответ 2

базы pimpl на указателях, и вы можете установить их в любое место, где выделены ваши объекты. Это также может быть статическая таблица объектов, объявленных в файле cpp. Главным моментом pimpl является сохранение стабильности интерфейсов и скрытие реализации (и используемых им типов).

Ответ 3

Если вы можете использовать boost, рассмотрите boost::optional<>. Это позволяет избежать затрат на динамическое распределение, но в то же время ваш объект не будет создан, пока вы не сочтете необходимым.

Ответ 4

Один из способов - иметь массив char [] в вашем классе. Сделайте его достаточно большим, чтобы ваш Impl мог поместиться, а в вашем конструкторе создайте экземпляр вашего Impl в своем массиве с новым местом размещения: new (&array[0]) Impl(...).

Вы также должны убедиться, что у вас нет проблем с выравниванием, возможно, если ваш массив char [] является членом объединения. Это:

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

например, убедитесь, что выравнивание array[0] будет подходящим для int, double или указателя.

Ответ 5

Смотрите Идиома Fast Pimpl и The Joy of Pimpls об использовании фиксированного распределителя вместе с идиомой pimpl.

Ответ 6

Точка использования pimpl заключается в том, чтобы скрыть реализацию вашего объекта. Это включает в себя размер истинного объекта реализации. Однако это также делает неудобным избегать динамического распределения - чтобы зарезервировать достаточное пространство стека для объекта, вам нужно знать, насколько велик объект.

Типичным решением является использование динамического распределения и передача ответственности за выделение достаточного пространства для (скрытой) реализации. Однако это невозможно в вашем случае, поэтому нам понадобится другой вариант.

Один из таких параметров использует alloca(). Эта малоизвестная функция выделяет память в стеке; память будет автоматически освобождена, когда функция выйдет из области действия. Это не переносимый С++, однако многие реализации С++ поддерживают его (или вариацию этой идеи).

Обратите внимание, что вы должны выделять объекты pimpl'd с помощью макроса; alloca() должен быть вызван для получения необходимой памяти непосредственно из собственной функции. Пример:

// 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();
}

Это, как вы видите, делает синтаксис довольно неудобным, но он выполняет аналог pimpl.

Если вы можете жестко задавать размер объекта в заголовке, также можно использовать массив char:

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;
// ...
}

Это менее чисто, чем описанный выше подход, так как вы должны менять заголовки всякий раз, когда изменяется размер реализации. Тем не менее, он позволяет использовать обычный синтаксис для инициализации.

Вы также можете реализовать теневой стек, то есть дополнительный стек, отдельный от обычного стека С++, в частности для хранения объектов pImpl'd. Это требует очень тщательного управления, но, должным образом завернутый, он должен работать. Этот тип находится в серой зоне между динамическим и статическим распределением.

// 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();
}

При таком подходе крайне важно убедиться, что вы НЕ используете теневой стек для объектов, где объект-оболочка находится в куче; это нарушит предположение, что объекты всегда уничтожаются в обратном порядке создания.