user1709708
user1709708

Reputation: 1577

How to achieve logical constness with the PIMPL idiom

Imagine a typical realization of the PIMPL idiom:

class ObjectImpl;

class Object
{
  Object(ObjectImpl* object_impl)
    : _impl(object_impl);

private:      
  ObjectImpl* _impl;
};

What I'm looking for is a way to reuse the same implementation to wrap a type T that's either ObjectImpl or const ObjectImpl but nothing else:

class ObjectImpl;

class Object
{
  Object(T* object_impl)
    : _impl(object_impl);

private:
  // T being either ObjectImpl or const ObjectImpl
  T* _impl;
};

What I'm trying to achieve is retaining logical constness through the PIMPL interface so that I'm disallowed by the compiler to call non-const methods on an Object wrapping a const ObjectImpl*.

It's basically just this trick borrowed from one of Scott Meyers Effective C++ books but with an added layer of abstraction:

struct SData
{
  const Data* data() const { return _data; }
  Data* data() { return _data; }

private:
  Data* _data:
};

Of course I could copy the entire class into a class ConstObject and have it wrap a const* Object instead of an Object* but I'm obviously trying to prevent code duplication.

I've also thought about templates but they seem a bit overkill for the task at hand. For one, I want T to only be either ObjectImpl or const ObjectImpl. Secondly, templates seem to work against the idea of PIMPL when exported as a DLL interface. Is there a better solution to go with?

Upvotes: 0

Views: 291

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

CRTP.

template<class Storage>
struct const_Object_helper {
  Storage* self() { return static_cast<D*>(this); }
  Storage const* self() const { return static_cast<D*>(this); }
  // const access code goes here, get it via `self()->PImpl()`
};
struct const_Object: const_Object_helper<const_Object> {
  const_Object( objectImpl const* impl ):pImpl(impl) {}
private:
  objectImpl const* pImpl = nullptr;
  objectImpl const* PImpl() const { return pImpl; }
  template<class Storage>
  friend struct const_Object_helper;
};
struct Object: const_Object_helper<Object> {
  // put non-const object code here
  Object( objectImpl* impl ):pImpl(impl) {}
  operator const_Object() const {
    return {PImpl()}; // note, a copy/clone/rc increase may be needed here
  }
private:
  objectImpl* pImpl = nullptr;
  objectImpl const* PImpl() const { return pImpl; }
  objectImpl* PImpl() { return pImpl; }
  template<class Storage>
  friend struct const_Object_helper;
};

This is the zero runtime overhead version, but requires the implementation of const_Object_helper and Object_helper to be exposed. As it just involves forwarding stuff to the actual impl, this seems relatively harmless.

You can remove that need by replacing the CRTP part of the helpers with a pure-virtual objectImpl const* get_pimpl() const = 0 and objectImpl* get_pimpl() = 0, then implement them in the derived types.

Another, somewhat crazy, approach would be to use an any augmented with type-erased operations (you also want to teach the type erasure mechanism about const, and that a super_any with fewer interfaces can be implicitly converted over without doing another layer of wrapping).

Here we define certain operations, say print and dance and boogie:

auto const print = make_any_method<void(std::ostream&), true>(
  [](auto&&self, std::ostream& s) {
    s << decltype(self)(self);
  }
);
auto const dance = make_any_method<void()>(
  [](auto&&self) {
    decltype(self)(self).dance();
  }
);
auto const dance = make_any_method<double(), true>(
  [](auto&&self) {
    return decltype(self)(self).boogie();
  }
);

Now we create two types:

using object = super_any< decltype(print), decltype(dance), decltype(boogie) > object;
using const_object = super_any< decltype(print), decltype(boogie) >;

Next, augment the super_any with the ability to assign-from sources with strictly weaker requirements.

Our object o; can (o->*dance)(). Our const_object co; can double d = (co->*boogie)();.

Anything can be stored in an object that supports the operations described by print, boogie and dance, plus the requirements of any (copy, destroy, assign). Anything at all.

Similarly, the const_object supports anything that can be described by print and boogie and copy/destroy/assign.

Derived types from object or const_object can add operator overloading features easily.

This technique is advanced. You can use boost::type_erasure to do it, probably slicker than this sketch.

Upvotes: 2

Sam Varshavchik
Sam Varshavchik

Reputation: 118310

I would suggest the following general design pattern. It wastes an additional pointer, but will enforce the requirement that the const object will be able to only access const methods of the private object:

class ObjectImpl;

class const_Object {

public:

  const_Object(const ObjectImpl* object_impl)
    : _impl(object_impl);

  // Only const methods

private:      
  const ObjectImpl* _impl;
};

class Object : public const_Object
{
  Object(ObjectImpl* object_impl)
    : const_Object(object_impl), _impl(object_impl);

  // non-const methods go here.

private:      
  ObjectImpl* _impl;
};

Upvotes: 2

Related Questions