Reputation: 1577
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
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
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