Pimpl идиома без использования динамического выделения памяти - PullRequest
28 голосов
/ 07 февраля 2011

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

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

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

Ответы [ 6 ]

25 голосов
/ 07 февраля 2011

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

Я бы предложил подход с использованием нового класса C ++ 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
}

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

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

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

8 голосов
/ 07 февраля 2011

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

4 голосов
/ 07 февраля 2011

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

3 голосов
/ 07 февраля 2011

См. Быстрая идиома Pimpl и Радость Pimpls об использовании фиксированного распределителя вместе с идиомой pimpl.

3 голосов
/ 07 февраля 2011

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

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

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

Например,

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

1 голос
/ 07 февраля 2011

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

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

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

Обратите внимание, что вы должны размещать объекты 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;
// ...
}

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

Вы также можете реализовать теневой стек, то есть вторичный стек, отдельный от обычного стека C ++, специально для хранения объектов 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();
}

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

...