An attempt to generalize the Pimpl idom with properly separated interface and implementation

Given I've been using this Pimpl idiom quite extensively lately I've noticed writing the same scaffolding over and over again. Being the lazy bugger as I am I looked around if there was anything available. Turns out there were proposals floated around (like Asger Mangaard's dated around May 2006). However, I did not see anything that would preserve idiom's original purpose/value - the separation of interface and implementation. So, I jogged something with that in mind. That seemed to do the job for me as something tedious like class Test { public: Test (int); Test (int, int); int get() const; bool operator==(Test const& p) const { return impl_ == p.impl_; } bool operator!=(Test const& p) const { return impl_ != p.impl_; } operator bool() const { return implementation_; } void swap(pimpl& that) { impl_.swap(that.impl_); } private: struct Internal; boost::shared_ptr<Internal> impl_; }; shrunk down do pure interface struct Test : public boost::pimpl<Test> { Test (int); Test (int, int); int get() const; }; with implementation details safely tacked away in an implementation file. However, it looked suspiciously easy. So, I am throwing it out for all the respected community to see (uploaded as vb_pimpl_0.1.zip) and to set me straight. Is there any value in that basic idea? Don't hold back your criticism. Regards, Vladimir.

Vladimir Batov wrote:
Given I've been using this Pimpl idiom quite extensively lately [...]
class Test { public:
Test (int); Test (int, int);
int get() const;
bool operator==(Test const& p) const { return impl_ == p.impl_; } bool operator!=(Test const& p) const { return impl_ != p.impl_; } operator bool() const { return implementation_; }
void swap(pimpl& that) { impl_.swap(that.impl_); }
private:
struct Internal; boost::shared_ptr<Internal> impl_;
Why use a boost::shared_ptr? You want your implementation details to be shared across all 'Test's? What you need is more like a deep-copying smart pointer.

Why use a boost::shared_ptr?
shared_ptr is an extremely versatile and poweful gadget. One (among others) thing it handles well is incomplete classes for implementation hiding. Maybe there are other smart pointers doing that sort of thing cheaper. I am not sure.
You want your implementation details to be shared across all 'Test's? What you need is more like a deep-copying smart pointer.
It certainly depends on application. Like if I have a GUI widget, then I do not want to deep-copy it. If I have static read-only configuration data, I do not want deep-copying those either. Deep-copying vs. shallow-copying is a separate issue that is likely to be addressed via an additional policy template parameter. I did not want to blow up my example with all the bells and whistles for smaller code foot-print and so that the main idea stands out clearer. Best, Vladimir.

Vladimir Batov wrote:
shared_ptr is an extremely versatile and poweful gadget. One (among others) thing it handles well is incomplete classes for implementation hiding. Maybe there are other smart pointers doing that sort of thing cheaper. I am not sure.
Here is a simple deep-copying smart pointer. template<typename T> struct copy_ptr { copy_ptr() : ptr(0) { } explicit copy_ptr(T* ptr_) : ptr(ptr_) { } ~copy_ptr() { delete ptr; } copy_ptr(const copy_ptr& p) ptr(p.ptr ? new(ptr) T(*p.ptr) : 0) { } copy_ptr& operator=(const copy_ptr& p) { if(ptr) { if(p.ptr) { *ptr = *p.ptr; } else { delete ptr; ptr = 0; } } else if(p.ptr) { new(ptr) T(*p.ptr); } } void swap(copy_ptr& p) { T* tmp = ptr; ptr = p.ptr; p.ptr = ptr; } T* operator->() { return ptr; } const T* operator->() const { return ptr; } T& operator*() { return *ptr; } const T& operator*() const { return *ptr; } T* get() { return ptr; } const T* get() const { return ptr; } private: T* ptr; }; Could hardly be simpler and more lightweight. (Well, not allowing null pointers would certainly simplify it actually) That is nothing compared to the massive overhead of using shared_ptr.
You want your implementation details to be shared across all 'Test's? What you need is more like a deep-copying smart pointer.
It certainly depends on application. Like if I have a GUI widget, then I do not want to deep-copy it. If I have static read-only configuration data, I do not want deep-copying those either.
Deep-copying vs. shallow-copying is a separate issue that is likely to be addressed via an additional policy template parameter. I did not want to blow up my example with all the bells and whistles for smaller code foot-print and so that the main idea stands out clearer.
When you copy an object, modifying the copy shouldn't trigger any visible side-effect in the original one. That is why sharing implementation details can only be done if data is read-only. Thus, shallow-copy certainly cannot be applied by default : it's implementation dependent ; so it's no good for something that's supposed to be a generic idiom for implementations. There is no need for a policy either, since you could just use shared_ptr in your implementation type if you want something to be shared and not copied. (that actually gives you more flexibility for the implementation than setting a policy in the interface, which would set the ABI) So you just need a deep-copying smart pointer like the one I provided earlier.

I don't think that copy_ptr class handles deletion of a pointer to an incomplete type properly.

Joseph Gauterin wrote:
I don't think that copy_ptr class handles deletion of a pointer to an incomplete type properly.
Actually, it's not the only problem, copying or operator= doesn't work too with incomplete types. I wasn't aware of this issues. One way to fix that would be to declare functions for deletion and cloning and then define them along with the internal structure.

Mathias,
Here is a simple deep-copying smart pointer. [snip] Could hardly be simpler and more lightweight.
The simplicity of your example and the relative complexity of shared_ptr are there for a reason. Apart from its obvious purpose (ref.-counting) the shared_ptr provides more features that are important for my aim of Pimpl generalization. My suspicion is that your implementation (unlike shared_ptr) does not handle incomplete classes well for implementation hiding. Something that is important for Pimpl.
That is nothing compared to the massive overhead of using shared_ptr.
Well, I feel that "the massive overhead" is debatable. If you refer to implementation complexity, it's there for a reason and is not an issue as it's already there. If you refer to perfomance, then it's not a clear cut as it very much depends on an application and the usage pattern. [snip]
That is why sharing implementation details can only be done if data is read-only.
Well, I am not sure I can agree as sharing certainly can be done when data are not read-only. Multi-threaded applixations certainly do not only work with read-only data. That's where data access serialization comes in. However, I am not sure it's relevant to our Pimpl topic. [snip]
There is no need for a policy either, since you could just use shared_ptr in your implementation type if you want something to be shared and not copied. (that actually gives you more flexibility for the implementation than setting a policy in the interface, which would set the ABI)
Thanks, I am not (yet) convinced that I can apply that to my Pimpl implementation (without exposing implementation detail) but I'll cetainly have a look if that might could be done. Thanks, Vladimir.

On 10/6/07, Vladimir Batov <batov@people.net.au> wrote:
Mathias,
Here is a simple deep-copying smart pointer. ... That is nothing compared to the massive overhead of using shared_ptr.
Well, I feel that "the massive overhead" is debatable. If you refer to implementation complexity, it's there for a reason and is not an issue as it's already there. If you refer to perfomance, then it's not a clear cut as it very much depends on an application and the usage pattern.
When it comes to performance, I think the key thing to understand about shared_ptr is that copying them or having copies go out of scope are the expensive operations. If you're just holding shared_ptrs to manage object lifetime you're fine. But if you start passing them around all over the place and making lots of short-lived copies, this is where you might run into trouble. -- Caleb Epstein

Vladimir Batov wrote:
Well, I feel that "the massive overhead" is debatable.
Size overhead, runtime overhead, multi-threading issues due to sharing...
Well, I am not sure I can agree as sharing certainly can be done when data are not read-only.
Of course, but in that case it's not just a mere implementation detail, as what pimpl is.

On Saturday 06 October 2007 23:10:52 Mathias Gaunard wrote:
Vladimir Batov wrote:
shared_ptr is an extremely versatile and poweful gadget. One (among others) thing it handles well is incomplete classes for implementation hiding. Maybe there are other smart pointers doing that sort of thing cheaper. I am not sure.
Here is a simple deep-copying smart pointer.
Where from? I hope that one is not in productive use, because...
template<typename T> struct copy_ptr { [...] explicit copy_ptr(T* ptr_) : ptr(ptr_) { }
...this should use std::auto_ptr to make clear that ownership is transferred and...
copy_ptr(const copy_ptr& p) ptr(p.ptr ? new(ptr) T(*p.ptr) : 0) { }
copy_ptr& operator=(const copy_ptr& p) { if(ptr) { if(p.ptr) { *ptr = *p.ptr; } else { delete ptr; ptr = 0; } } else if(p.ptr) { new(ptr) T(*p.ptr); } }
...it looks like you are using placement new on a null pointer in the assignment and on an uninitialised pointer in the copy constructor, or am I missing something?
void swap(copy_ptr& p) { T* tmp = ptr; ptr = p.ptr; p.ptr = ptr; }
How about std::swap( p.ptr, ptr); ?
T* operator->() { return ptr; }
const T* operator->() const { return ptr; }
Why? I mean since when is the CV qualification of a pointer relevant for the CV qualification of the pointee? Uli

Ulrich Eckhardt wrote:
Here is a simple deep-copying smart pointer.
Where from? I hope that one is not in productive use, because...
I just wrote it as an example.
...it looks like you are using placement new
I wrote this quite late, and mixed things up because initially I felt like writing things based on an allocator, but decided not to make things too complex. Obviously it should have been regular new, and not placement new.
Why? I mean since when is the CV qualification of a pointer relevant for the CV qualification of the pointee?
I thought it was relevant for private implementations. If the object is const, the implementation details should be const too.

[snip] There has once been a submission about pimpl to Boost: http://lists.boost.org/boost-announce/2006/05/0090.php It was refused. I can't comment any further because I didn't really follow the reviews, but you can have a look in the mailing list archives. -- Francois Duranleau

Yeah, I surely looked at that one first back then when it was submitted and now and I mention that in my original post. The major issue with Asger Mangaard's implementation was that in order to generalize he had to sacrifice the very reason the Pimpl existed -- opaqueness of implementation details. Best, Vladimir. ----- Original Message ----- From: "François Duranleau" <duranlef@iro.umontreal.ca> Newsgroups: gmane.comp.lib.boost.devel To: <boost@lists.boost.org> Sent: Saturday, October 06, 2007 5:18 AM Subject: Re: An attempt to generalize the Pimpl idom with properly separated interface and implementation
[snip]
There has once been a submission about pimpl to Boost:
http://lists.boost.org/boost-announce/2006/05/0090.php
It was refused. I can't comment any further because I didn't really follow the reviews, but you can have a look in the mailing list archives.
-- Francois Duranleau

Vladimir Batov wrote:
Given I've been using this Pimpl idiom quite extensively lately I've noticed writing the same scaffolding over and over again.
Maybe we need something with a mixture of features from scoped_ptr and shared_ptr. scoped_ptr does everything that I need for a pimpl, except that it can't delete the incomplete implementation type. shared_ptr can delete the incomplete implementation, but it has the unneeded overhead of reference counting and thread safety issues. So, can the incomplete deletion feature of shared_ptr be extracted and added to scoped_ptr? I have just had a quick look at the shared_ptr implementation to see how deletion works, but it's too clever for me to understand.... Regards, Phil.

Phil Endecott:
Vladimir Batov wrote:
Given I've been using this Pimpl idiom quite extensively lately I've noticed writing the same scaffolding over and over again.
Maybe we need something with a mixture of features from scoped_ptr and shared_ptr. scoped_ptr does everything that I need for a pimpl, except that it can't delete the incomplete implementation type. shared_ptr can delete the incomplete implementation, but it has the unneeded overhead of reference counting and thread safety issues. So, can the incomplete deletion feature of shared_ptr be extracted and added to scoped_ptr? I have just had a quick look at the shared_ptr implementation to see how deletion works, but it's too clever for me to understand....
Alan Griffiths has written a similar smart pointer, arg::grin_ptr: http://www.octopull.demon.co.uk/arglib/TheGrin.html

Peter Dimov wrote:
Phil Endecott:
Vladimir Batov wrote:
Given I've been using this Pimpl idiom quite extensively lately I've noticed writing the same scaffolding over and over again.
Maybe we need something with a mixture of features from scoped_ptr and shared_ptr. scoped_ptr does everything that I need for a pimpl, except that it can't delete the incomplete implementation type. shared_ptr can delete the incomplete implementation, but it has the unneeded overhead of reference counting and thread safety issues. So, can the incomplete deletion feature of shared_ptr be extracted and added to scoped_ptr? I have just had a quick look at the shared_ptr implementation to see how deletion works, but it's too clever for me to understand....
Alan Griffiths has written a similar smart pointer, arg::grin_ptr:
Thanks Peter, that's exactly what's needed. And very well described by Alan on that page. It seems that the trick needed to allow incomplete types to be deleted is the same one that allows a shared_ptr to be given a custom deleter, e.g. so that I can free() things that have been malloc()ed by C code. A smart pointer with these features would be great for Boost. Vladimir, would you like to revise your pimpl template to use this? Regards, Phil.

Phil, thank you for your encouragement and Peter, thank you for the pointers. Much and truly appreciated. I'll definitely have a look at both Alan's and Peter's implementations if I can make use of those. It's Saturday -- my family-duties day -- and I am just running out the door. After a rrrreally quick look the grip_ptr seems to provide a deep-copy semantics without exposing private implementation. It that is the case, then Alan and Peter (who implemented a similar thing) are definitely smarter than I am as I was not able to figure out how to achieve that (without exposing implementation details). Then I'll steal :-) their idea (with proper acknowledments :-)). That will have to be done in addition to my currently proposed shared_ptr-based implementation as, for example, my current usage pattern is such that I use one data repository/implementation and share/pass many smart-pointer type instances pointing to that sole implementation. Will let you know how it all fired soon. Best, Vladimir. "Phil Endecott" <spam_from_boost_dev@chezphil.org> wrote in message news:1192106650916@dmwebmail.japan.chezphil.org...
Peter Dimov wrote:
Phil Endecott:
Vladimir Batov wrote:
Given I've been using this Pimpl idiom quite extensively lately I've noticed writing the same scaffolding over and over again.
Maybe we need something with a mixture of features from scoped_ptr and shared_ptr. scoped_ptr does everything that I need for a pimpl, except that it can't delete the incomplete implementation type. shared_ptr can delete the incomplete implementation, but it has the unneeded overhead of reference counting and thread safety issues. So, can the incomplete deletion feature of shared_ptr be extracted and added to scoped_ptr? I have just had a quick look at the shared_ptr implementation to see how deletion works, but it's too clever for me to understand....
Alan Griffiths has written a similar smart pointer, arg::grin_ptr:
Thanks Peter, that's exactly what's needed. And very well described by Alan on that page.
It seems that the trick needed to allow incomplete types to be deleted is the same one that allows a shared_ptr to be given a custom deleter, e.g. so that I can free() things that have been malloc()ed by C code. A smart pointer with these features would be great for Boost.
Vladimir, would you like to revise your pimpl template to use this?
Regards, Phil.

Peter, Thank you for the pointers. I had a look at your and Alan Griffiths implementations of smart pointer classes with value/deep-copy rather than pointer/shallow-copy semantics. That's the technique I've been looking for. Somehow it did not occur to me to extend shared_ptr's deletion technique of an incomplete type onto copying. Shame on me. Looks like working while my daughter is on my lap is not exactly effective. :-) Anyway, I see you'd been preparing your implementation for Boost but it does not seem to have ever made it. That's unfortunate. How come it was abandoned and sent to the fringes of Yahoo archives? We seem certain to need a smart pointer with value semantics and the interface to match shared_ptr. There must have been a reason for not proceeding with such a smart pointer. Or was it? Please enlighten me so that I do not spend time writing such a pointer just to realize I missed a simple reason we do not need it. Best, Vladimir. ----- Original Message ----- From: "Peter Dimov" <pdimov@pdimov.com>
Alan Griffiths has written a similar smart pointer, arg::grin_ptr:
http://www.octopull.demon.co.uk/arglib/TheGrin.html
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

Vladimir Batov: ...
Anyway, I see you'd been preparing your implementation for Boost but it does not seem to have ever made it. That's unfortunate. How come it was abandoned and sent to the fringes of Yahoo archives?
I have a "policy" of not proposing libraries that I don't use. impl_ptr was a nice experiment, but shared_ptr happens to satisfy all my implementation-hiding needs. :-)

From: "Peter Dimov" <pdimov@pdimov.com>
Anyway, I see you'd been preparing your implementation for Boost but it does not seem to have ever made it. That's unfortunate. How come it was abandoned and sent to the fringes of Yahoo archives?
I have a "policy" of not proposing libraries that I don't use. impl_ptr was a nice experiment, but shared_ptr happens to satisfy all my implementation-hiding needs. :-)
That's fair enough. I myself certainly always use shared_ptr-based Pimpl. However, due to specificity of my task I tend to use Pimpls with pointer semantics only. In this discussion thread as soon as I mentioned Pimpl people asked for value semantics and I believe that's reasonable. Are you saying you've been using shared_ptr for Pimpls with value semantics? Then, I guess, you'd have to explicitly write Pimpl(Pimpl const&), Pimpl::op=(), Pimpl::op==(). Something straightforward like template<class T> class Pimpl { public: Pimpl(Pimpl const that) : impl_(new T(*that.impl_)) {} Pimpl& operator=(Pimpl const& that) const { *impl_ = *that.impl_; return *this; } bool operator==(Pimpl const& that) const { return *impl_ = *that.impl_; } private: class Implementation; boost::shared_ptr<Implementation> impl_; }; does not cut it as it does not compile unless Pimpl::Implementation is visible. So, we have to apply the same incomple-type management technique as deployed in shared_ptr and your other impl_ptr. Like template<class T> class Pimpl { public: Pimpl(Pimpl const that) : impl_(trait_->copy(*that.impl_)) {} Pimpl& operator=(Pimpl const& that) const { trait_->assign(*impl_, *that.impl_); return *this; } bool operator==(Pimpl const& that) const { return trait_->compare(*impl_, *that.impl_); } private: class Implementation; boost::shared_ptr<Implementation> impl_; trait* trait_; }; and that seems like a serious hassle unless generalized. So, it feels the Pimpl situation is begging for your impl_ptr. Don't you agree? Unfortunately, neither your impl_ptr nor Alan Griffith grin_ptr seem to be complete. Alan's implementation is very basic and does not seem to handle run-time polymorphic classes. Your impl_ptr is better in that regard and closer to shared_ptr (for obvious reasons :-)). However, dynamic_traits extends incomplete-type management only onto copy when I feel it needs to do the same for assignment and comparison. I do not feel impl_ptr's approach to assignment via deletion and copy construction is 100% kosher. Ideally, impl_ptr and shared_ptr ought to share the same scaffolding with different deep/shallow-copy policies. It's sad, it has not worked out that way. Getting back to Pimpl, I feel that a thin interface layer on top of shared_ptr or impl_ptr would alleviate much annoyance while writing Pimpls. It can be as simlpe as class Test1 : boost::pimpl<Test1, pointer_semantics> { only Test interface here }; class Test2 : boost::pimpl<Test2, value_semantics> { only Test interface here }; So, I guess, my question is how would you suggest I approach the issue? I'd really like to hear you view on the subject. Best, Vladimir.

Has anyone looked into a 'FastPimpl'? (ie see http://www.gotw.ca/publications/mill05.htm, including the list of reasons not to). I wrote a generic FastPimpl once that looked something like this: //h file // forward declare: class MyImplementation; class Wrapper { //... private: FastPimpl<MyImplementation, 24> m_Impl; // m_Impl works like a pointer, but isn't. It is embedded. }; The '24' above is a guess at the required size of MyImplementation. If the size is wrong, the cpp doesn't compile. My version overloaded copy, assign, etc, to 'do the right thing' ie pass it on the underlying MyImplementation. Just a thought. Tony

Vladimir Batov: ...
That's fair enough. I myself certainly always use shared_ptr-based Pimpl. However, due to specificity of my task I tend to use Pimpls with pointer semantics only. In this discussion thread as soon as I mentioned Pimpl people asked for value semantics and I believe that's reasonable. Are you saying you've been using shared_ptr for Pimpls with value semantics?
My use cases so far have always been of the form // foo.hpp: namespace foo { class impl; typedef shared_ptr<impl> handle; handle create( ... ); void operation( handle, ... ); } // foo.cpp: #include "foo.hpp" #include <windows.h> // this is why we bother with a pimpl ... <windows.h> is just an example, of course, there are many other headers that one would like to hide behind a pimpl. This also creates a stable binary interface. I think I had a need for a value-based pimpl once or twice, but I don't remember how I handled it. Probably the old-fashioned way.
Unfortunately, neither your impl_ptr nor Alan Griffith grin_ptr seem to be complete. Alan's implementation is very basic and does not seem to handle run-time polymorphic classes. Your impl_ptr is better in that regard and closer to shared_ptr (for obvious reasons :-)). However, dynamic_traits extends incomplete-type management only onto copy when I feel it needs to do the same for assignment and comparison. I do not feel impl_ptr's approach to assignment via deletion and copy construction is 100% kosher.
An interesting question. I haven't given assignment much thought since in the Old C++ Days (before move), if you were willing to expose a heavy copy constructor that hits the heap, once could reasonably conclude that the class (or rather the container it's being put in) is not performance-critical. So implementing assignment in terms of copy construction wasn't that bad, efficiency-wise, and saved the user from having to make the implementation class Assignable.
participants (9)
-
Caleb Epstein
-
François Duranleau
-
Gottlob Frege
-
Joseph Gauterin
-
Mathias Gaunard
-
Peter Dimov
-
Phil Endecott
-
Ulrich Eckhardt
-
Vladimir Batov