Pimpl idiom without using dynamic memory allocation
Asked Answered
F

7

35

we want to use pimpl idiom for certain parts of our project. These parts of the project also happen to be parts where dynamic memory allocation is forbidden and this decision is not in our control.

So what i am asking is, is there a clean and nice way of implementing pimpl idiom without dynamic memory allocation?

Edit
Here are some other limitations: Embedded platform, Standard C++98, no external libraries, no templates.

Fagin answered 7/2, 2011 at 13:38 Comment(6)
Whats the point of pimpl without dynamic allocation? pimpl's primary use is to make the lifetime of dynamic objects managable. If you don't have lifetime management issues, then just pass the reference to the static/stack scoped object around directly.Coolant
I think the primary use of pimpl is hiding implementation details, hence the name "pointer to implementation idiom".Fagin
@Chris: we don't need pimpl to manage lifetime of objects. Just use a smart pointer (or write the object to follow the RAII idiom in the first place). pimpl is about hiding the internals of a class.Toastmaster
how can someone with 23k rep misunderstand a basic idiom so egregiouslyDartmouth
@Dartmouth c++ is a big language, lots of people only use it in an embedded space which means they might never learn about pimpl, because it is largely useless there.Aluminium
@FantasticMrFox It's perfectly fair for someone not to know what it is. But then they shouldn't post false assertions about what it is for.Dartmouth
C
31

Warning: the code here only showcases the storage aspect, it is a skeleton, no dynamic aspect (construction, copy, move, destruction) has been taken into account.

I would suggest an approach using the C++0x new class aligned_storage, which is precisely meant for having raw 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;
};

In the source, you then implement a check:

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
}

This way, while you'll have to change the alignment immediately (if necessary) the size will only change if the object changes too much.

And obviously, since the check is at compilation time, you just cannot miss it :)

If you do not have access to C++0x features, there are equivalents in the TR1 namespace for aligned_storage and alignof and there are macros implementations of static_assert.

Cornellcornelle answered 7/2, 2011 at 14:20 Comment(12)
Why is 10% tolerance margin needed here?Disbud
@Gart: any change in the size of Foo introduces a binary incompatibility, which is what we are trying to prevent here. You thus need StorageSize to be superior to sizeof(Impl) and stable, thus you will probably slightly oversize it so as to be able to add fields to Impl later on. However you may overshoot too much and end up with a very large object for... nothing, so I suggest to check that you do not end up with an overly big object either, using this 10% margin.Cornellcornelle
I needed to call new( &_storage )Impl(); in the constructor to get Pimpl members to initialise correctly.Arose
I also needed to call reinterpret_cast< Impl* >( &_storage )->~Impl(); in the destructor to avoid leaking memory.Arose
@SurvivalMachine: oh my, yes, the code here is very much incomplete and only discusses the storage aspect, no dynamic aspect has been discussed (construction, copy, move, destruction, etc...). I'll add a warning.Cornellcornelle
The problem with this approach that I see is: how do you know what XXX and YYY are, in a portable way? And by portable I also mean portable across multiple versions of the same compiler on the same architecture.Hannigan
@FabioA.: That's the point of possibly having a tolerance margin. Alignment requirements are generally driven by the target CPU and actually vary quite rarely (most often 4 or 8 bytes), it would be fine to simply ensure that the alignment is higher than the required one. The size varies more, but as long as you are ready to adjust (or even lift entirely) the upper bound, there is no issue.Cornellcornelle
I think this is a good solution. I think it can be packaged up in a template like this: godbolt.org/z/EcGfW1Caslon
@Ben: Nice! I don't think that disabling copy/move is necessary. The implementation should be possible -- though of course it would only be usable where value_type is defined.Cornellcornelle
To refute Sutter's "Why Attempt #3 is Deplorable" gotw.ca/gotw/028.htm (which is pre-C++11, I think): 1. I dealt with alignment (and could do better using std::align to allow the value to be offset in the buffer) 2. Brittleness: It's now easy to make it statically safe. 3. Maintenance Cost: There are cases where the size won't change but the headers required are expensive. 4. Wasted space: Sometimes I don't care. 5. I'll leave unanswered. My point is I do have a few classes I want as members of vocabulary types but that pull in huge headers. This could fix that; modules may too.Caslon
@Ben: Indeed, modules should obsolete the "Compilation Firewall" aspects of PIMPL, and therefore the InlinePimpl... they're still not there yet though, so I think your implementation can serve you well in the mean time :)Cornellcornelle
A related class that I've been surprised boost doesn't have is small_any<Size, Align=1, Alloc=std::allocator<std::byte>> and static_any<Size, Align> which would be like boost::container::small_vector<T, N, Alloc> and ...::static_vector<T, N> but for any type with small-object optimization and statically-required-small-object-only respectively. (Or likewise with a forward-declared variant. Basically my solution could be considered static_forward_declared_variant<T, Size, Align>.Caslon
A
11

pimpl bases on pointers and you can set them to any place where your objects are allocated. This can also be a static table of objects declared in the cpp file. The main point of pimpl is to keep the interfaces stable and hide the implementation (and its used types).

Adelladella answered 7/2, 2011 at 13:41 Comment(4)
I think that this is the best approach for our case but i don't think that it will be nice and clean like standard pimpl.Fagin
IMHO the only downside of this approach is that you have to agree on a maximum number of objects of that type in advance/at compile time. For all other aspects I can think of, the goals of pimpl are reached.Adelladella
Having to decide in advance on the maximum number of objects is not a bug, it is a feature. It is one of the primary rationales behind rules that forbid dynamic memory allocation. Do this and you never run out of memory. And you never have to worry about fragmented heaps.Romany
Good point sbass to stress that out, my formulation was a bit negative regarding this aspect. +1Adelladella
M
4

See The Fast Pimpl Idiom and The Joy of Pimpls about using a fixed allocator along with the pimpl idiom.

Mattson answered 7/2, 2011 at 13:47 Comment(1)
I think writing a fixed allocator misses the whole point of "not using dynamic memory". It may not require dynamic memory allocation but it requires dynamic memory management, which i think is not different than overriding new and delete globally.Fagin
I
4

If you can use boost, consider boost::optional<>. This avoids the cost of dynamic allocation, but at the same time, your object will not be constructed until you deem necessary.

Incomprehension answered 7/2, 2011 at 14:11 Comment(2)
Sorry, we can't use boost, or any other external library :(Fagin
Why are you apologising, you can't help artificial constraints? :) Anyway, if you wanted to, it's pretty straight forward to strip out the code from boost::optional, the cleverest bit of the code is the aligned_storage structure which declares a character array taking into account alignment, then it's a simple placement new to construct.Incomprehension
G
3

One way would be to have a char[] array in your class. Make it large enough for your Impl to fit, and in your constructor, instantiate your Impl in place in your array, with a placement new: new (&array[0]) Impl(...).

You should also ensure you don't have any alignment problems, probably by having your char[] array a member of an union. This:

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

for instance, will make sure the alignment of array[0] will be suitable for an int, double or a pointer.

Graupel answered 7/2, 2011 at 13:42 Comment(5)
+1: Was writing a longer post, but this is basically it. You could write a second project that gets the size of the impl classes and instruments that into the containing classes, so you don't need to manually track every change.Tarton
not sure the members of the union are enough to guarantee alignmentMattson
That approach requires us to maintain the size of the char array whenever the implementation changes (and it may change frequently in some places). Also We can't make it big for future because memory is scarce.Fagin
@erelender: it could be done as a simple preprocessing task though. Compile the file that defines the "inner" class in a small test program which returns its size, and then write that size into the pimpl class definition. Alternatively, a static assert as suggested by @Matthieu M. could be used to alert you if the "predicted size is too small, so the code won't compile unless a valid size is chosen.Toastmaster
The union trick is not necessary now that std::aligned_storage exists (which might use it internally, but ehh, whatever). But a more fundamental problem here is how you said "will be suitable for an int, double or a pointer". For pointers, your example will only be guaranteed to be suitably aligned for a char* pointer. Remember that pointers to different types are not required to have the same sizes (or representations, or etc.)Dartmouth
D
1

The point of using pimpl is to hide the implementation of your object. This includes the size of the true implementation object. However this also makes it awkward to avoid dynamic allocation - in order to reserve sufficient stack space for the object, you need to know how big the object is.

The typical solution is indeed to use dynamic allocation, and pass the responsibility for allocating sufficient space to the (hidden) implementation. However, this isn't possible in your case, so we'll need another option.

One such option is using alloca(). This little-known function allocates memory on the stack; the memory will be automatically freed when the function exits its scope. This is not portable C++, however many C++ implementations support it (or a variation on this idea).

Note that you must allocate your pimpl'd objects using a macro; alloca() must be invoked to obtain the necessary memory directly from the owning function. Example:

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

This, as you can see, makes the syntax rather awkward, but it does accomplish a pimpl analogue.

If you can hardcode the size of the object in the header, there's also the option of using a char array:

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

This is less pure than the above approach, as you must change the headers whenever the implementation size changes. However, it allows you to use normal syntax for initialization.

You could also implement a shadow stack - that is, a secondary stack separate from the normal C++ stack, specifically to hold pImpl'd objects. This requires very careful management, but, properly wrapped, it should work. This sort of is in the grey zone between dynamic and static allocation.

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

With this approach it is critical to ensure that you do NOT use the shadow stack for objects where the wrapper object is on the heap; this would violate the assumption that objects are always destroyed in reverse order of creation.

Derringdo answered 7/2, 2011 at 14:36 Comment(0)
F
0

One technique I've used is a non-owning pImpl wrapper. This is a very niche option and isn't as safe as traditional pimpl, but it can help if performance is a concern. It may require some re-architecture to more functional like apis.

You can create a non-owning pimpl class, as long as you can (somewhat) guarantee the stack pimpl object will outlive the wrapper.

For ex.

/* header */
struct MyClassPimpl;
struct MyClass {
    MyClass(MyClassPimpl& stack_object); // Initialize wrapper with stack object.

private:
    MyClassPimpl* mImpl; // You could use a ref too.
};


/* in your implementation code somewhere */

void func(const std::function<void()>& callback) {
    MyClassPimpl p; // Initialize pimpl on stack.

    MyClass obj(p); // Create wrapper.

    callback(obj); // Call user code with MyClass obj.
}

The danger here, like most wrappers, is the user stores the wrapper in a scope that will outlive the stack allocation. Use at your own risk.

Frederiksberg answered 31/7, 2020 at 19:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.