开发者

Pimpl framework comments/suggestions requested

I've basically implemented a proposal, my question is, has it been done, and if so, where? And/or is there a better way to do what I'm doing? Sorry about the length of this post, I didn't know a better way to explain my approach other than providing the code.

I previously asked the question pimpl: Avoiding pointer to pointer with pimpl?

To explain that question again here, basically, lets say we've got an interface interface and an implementation impl. Further, like the pimpl idiom, we want to be able to separately compile the impl.

Now a way to do this in c++0x is to make a say unique_ptr in the interface which points to the impl. The actual implementation the methods of interface are not included in main.cpp, they are compiled separately in say interface.cpp, along with both the interface and implementation of impl.

We design this class as if the pointer is not there so it's effectively transparent to the user. We use the . notation to call methods, not the -> notation, and if we want to copy, we implement deep copy schematics.

But then I was thinking what if I actually wanted a shared pointer to this pimpl. I could just do shared_ptr<interface>, but then I'd have a shared_ptr to a unique_ptr, and I thought that was a bit silly. I could use shared_ptr instead of unique_ptr inside interface, but then it would still use the . notation calling functions, and wouldn't really look li开发者_JAVA技巧ke a pointer, so might surprise users when it shallow copies.

I began to think it would be good to have some generic template class that connected an interface X and a corresponding implementation Y for any compatible pair of X and Y, handling a lot of the pimpl boilerplate stuff.

So below is how I've attempted to do it.

First I'll start with main.cpp:

#include "interface.hpp"
#include "unique_pimpl.hpp"
#include "shared_pimpl.hpp"

int main()
{
  auto x1 = unique_pimpl<interface, impl>::create();
  x1.f();

  auto x2(x1);
  x2 = x1;

  auto x3(std::move(x1)); 
  x3 = std::move(x1);

  auto y1 = shared_pimpl<interface, impl>::create();
  y1->f();

  auto y2(y1);
  y2 = y1;

  auto y3(std::move(y1));
  y3 = std::move(y1);
}

Basically here, x1 is the standard unique_ptr pimpl implementation. x2 is actually a shared_ptr, without the double pointer caused by a unique_ptr. A lot of the assignments and constructors were for testing only.

Now interface.hpp:

#ifndef INTERFACE_HPP
#define INTERFACE_HPP

#include "interface_macros.hpp"

class impl;

INTERFACE_START(interface);

  void f();

INTERFACE_END;

#endif

interface_macros.hpp:

#ifndef INTERFACE_MACROS_HPP
#define INTERFACE_MACROS_HPP

#include <utility>

#define INTERFACE_START(class_name) \
template <class HANDLER> \
class class_name : public HANDLER \
{ \
public: \
  class_name(HANDLER&& h = HANDLER()) : HANDLER(std::move(h)) {} \
  class_name(class_name<HANDLER>&& x) : HANDLER(std::move(x)) {} \
  class_name(const class_name<HANDLER>& x) : HANDLER(x) {}

#define INTERFACE_END }

#endif

interface_macros.hpp just contains some boilerplate code which is required for the framework I've developed. The interface takes HANDLER as a template argument and makes it a base class, this constructors just ensure things are forwarded to the base HANDLER where the action happens. Of course interface itself will have no members and no constructors for it's on purpose, just some public member functions.

Now interface.cpp is our other file. It actually contains implementation of interface, and despite its name, also the interface and implementation of impl. I won't list the file in full yet, but the first think it includes is interface_impl.hpp (sorry about the confusing naming).

Here is interface_impl.hpp:

#ifndef INTERFACE_IMPL_HPP
#define INTERFACE_IMPL_HPP

#include "interface.hpp"
#include "impl.hpp"

template <class HANDLER>
void interface<HANDLER>::f() { this->get_impl().f(); }

#endif

Note the get_impl() method call. This is going to be provided by HANDLER later.

impl.hpp contains both the interface and implementation of impl. I could have separated these, but didn't see a need. Here is impl.hpp:

#ifndef IMPL_HPP
#define IMPL_HPP

#include "interface.hpp"
#include <iostream>

class impl
{
public:
  void f()  { std::cout << "Hello World" << std::endl; };
};

#endif

Now lets have a look at unique_pimpl.hpp. Remember this was included in main.cpp, so our main program has a definition of this.

unique_pimpl.hpp:

#ifndef UNIQUE_PIMPL_HPP
#define UNIQUE_PIMPL_HPP

#include <memory>

template
<
  template<class> class INTERFACE,
  class IMPL
>
class unique_pimpl
{
public:
  typedef IMPL impl_type;
  typedef unique_pimpl<INTERFACE, IMPL> this_type;
  typedef INTERFACE<this_type> super_type;

  template <class ...ARGS>
  static super_type create(ARGS&& ...args);
protected:
  unique_pimpl(const this_type&);
  unique_pimpl(this_type&& x);
  this_type& operator=(const this_type&);
  this_type& operator=(this_type&& p);
  ~unique_pimpl();

  unique_pimpl(impl_type* p);
  impl_type& get_impl();
  const impl_type& get_impl() const;
private:
  std::unique_ptr<impl_type> p_;
};

#endif

Here we will pass the template class INTERFACE (which has one parameter, HANDLER, which we will fill in here with unique_pimpl), and the IMPL class (which is in our case impl). This class is where the unique_ptr actually resides.

Now this here provides the get_impl() function that we were looking for. Our interface can call this function so it can forward calls to the implementation.

Lets have a look at unique_pimpl_impl.hpp:

#ifndef UNIQUE_PIMPL_IMPL_HPP
#define UNIQUE_PIMPL_IMPL_HPP

#include "unique_pimpl.hpp"

#define DEFINE_UNIQUE_PIMPL(interface, impl, type) \
template class unique_pimpl<interface, impl>; \
typedef unique_pimpl<interface, impl> type; \
template class interface< type >;

template < template<class> class INTERFACE, class IMPL> template <class ...ARGS>
typename unique_pimpl<INTERFACE, IMPL>::super_type 
unique_pimpl<INTERFACE, IMPL>::create(ARGS&&... args) 
  { return unique_pimpl<INTERFACE, IMPL>::super_type(new IMPL(std::forward<ARGS>(args)...)); }

template < template<class> class INTERFACE, class IMPL>
typename unique_pimpl<INTERFACE, IMPL>::impl_type& 
unique_pimpl<INTERFACE, IMPL>::get_impl() 
  { return *p_; }

template < template<class> class INTERFACE, class IMPL>
const typename unique_pimpl<INTERFACE, IMPL>::impl_type& 
unique_pimpl<INTERFACE, IMPL>::get_impl() const 
  { return *p_; }

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>::unique_pimpl(typename unique_pimpl<INTERFACE, IMPL>::impl_type* p) 
  : p_(p) {}

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>::~unique_pimpl() {}

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>::unique_pimpl(unique_pimpl<INTERFACE, IMPL>&& x) : 
  p_(std::move(x.p_)) {}

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>::unique_pimpl(const unique_pimpl<INTERFACE, IMPL>& x) : 
  p_(new IMPL(*(x.p_))) {}

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>& unique_pimpl<INTERFACE, IMPL>::operator=(unique_pimpl<INTERFACE, IMPL>&& x) 
  { if (this != &x) { (*this).p_ = std::move(x.p_); } return *this; }

template < template<class> class INTERFACE, class IMPL>
unique_pimpl<INTERFACE, IMPL>& unique_pimpl<INTERFACE, IMPL>::operator=(const unique_pimpl<INTERFACE, IMPL>& x) 
  { if (this != &x) { this->p_ = std::unique_ptr<IMPL>(new IMPL(*(x.p_))); } return *this; }

#endif

Now a lot of the above is just boiler plate code, and does what you expect. create(...) simply forwards to the constructor of impl, which otherwise wouldn't be visible to the user. Also there is a macro definition DEFINE_UNIQUE_PIMPL which we can use later on to instantiate the appropriate templates.

Now we can come back to interface.cpp:

#include "interface_impl.hpp"
#include "unique_pimpl_impl.hpp"
#include "shared_pimpl_impl.hpp"

// This instantates required functions

DEFINE_UNIQUE_PIMPL(interface, impl, my_unique_pimpl)

namespace
{
  void instantate_my_unique_pimpl_create_functions()
  {
    my_unique_pimpl::create();
  }
}

DEFINE_SHARED_PIMPL(interface, impl, my_shared_pimpl)

namespace
{
  void instantate_my_shared_pimpl_create_functions()
  {
    my_shared_pimpl::create();
  }
}

This makes sure all the appropriate templates are compiled instantate_my_unique_pimpl_create_functions() ensures we compile a 0-argument create and is otherwise never intended to be called. If impl had other constructors we wanted to call from main, we could define them here (e.g. my_unique_pimpl::create(int(0))).

Looking back up at main.cpp, you can now see how unique_pimpls can be created. But we can create other joining methods, and here is shared_pimpl:

shared_pimpl.hpp:

#ifndef SHARED_PIMPL_HPP
#define SHARED_PIMPL_HPP

#include <memory>

template <template<class> class INTERFACE, class IMPL>
class shared_impl_handler;

template < template<class> class INTERFACE, class IMPL>
class shared_pimpl_get_impl
{
public:
  IMPL& get_impl();
  const IMPL& get_impl() const;
};

template
<
  template<class> class INTERFACE,
  class IMPL
>
class shared_pimpl
{
public:
  typedef INTERFACE< shared_pimpl_get_impl<INTERFACE, IMPL> > interface_type;
  typedef shared_impl_handler<INTERFACE, IMPL> impl_type;
  typedef std::shared_ptr<interface_type> return_type;

  template <class ...ARGS>
  static return_type create(ARGS&& ...args);
};

#endif

shared_pimpl_impl.hpp:

#ifndef SHARED_PIMPL_IMPL_HPP
#define SHARED_PIMPL_IMPL_HPP

#include "shared_pimpl.hpp"

#define DEFINE_SHARED_PIMPL(interface, impl, type) \
template class shared_pimpl<interface, impl>; \
typedef shared_pimpl<interface, impl> type; \
template class interface< shared_pimpl_get_impl<interface, impl> >;

template <template<class> class INTERFACE, class IMPL>
class shared_impl_handler : public INTERFACE< shared_pimpl_get_impl<INTERFACE, IMPL> >, public IMPL 
{
  public:
    template <class ...ARGS>
    shared_impl_handler(ARGS&&... args) : INTERFACE< shared_pimpl_get_impl<INTERFACE, IMPL> >(), IMPL(std::forward<ARGS>(args)...) {}
};

template < template<class> class INTERFACE, class IMPL> template <class ...ARGS>
typename shared_pimpl<INTERFACE, IMPL>::return_type shared_pimpl<INTERFACE, IMPL>::create(ARGS&&... args) 
  { return shared_pimpl<INTERFACE, IMPL>::return_type(new shared_pimpl<INTERFACE, IMPL>::impl_type(std::forward<ARGS>(args)...)); }

template < template<class> class INTERFACE, class IMPL>
IMPL& shared_pimpl_get_impl<INTERFACE, IMPL>::get_impl() 
  { return static_cast<IMPL&>(static_cast<shared_impl_handler<INTERFACE, IMPL>& >(static_cast<INTERFACE< shared_pimpl_get_impl<INTERFACE, IMPL> >&>(*this))); }

template < template<class> class INTERFACE, class IMPL>
const IMPL& shared_pimpl_get_impl<INTERFACE, IMPL>::get_impl() const 
  { return static_cast<const IMPL&>(static_cast<const shared_impl_handler<INTERFACE, IMPL>& >(static_cast<const INTERFACE<shared_pimpl_get_impl<INTERFACE, IMPL> >&>(*this))); }

#endif

Note that create for shared_pimpl actually returns a real shared_ptr, without a double redirection. The static_cast in get_impl() are a mess, sadly I didn't know a better way to do it other than go two steps up the inheritance tree and then one down to the implementation.

I can imagine making other "HANDLER" classes for intrusive pointers for example, and even a simple stack allocated join which requires all header files to be included in the traditional way. That way users can write classes that are pimpl ready but not pimpl required.

You can download all the files from a zip here. They will extract to the current directory. You'll need to compile with something with some c++0x features, both gcc 4.4.5 and gcc 4.6.0 worked fine for me.

So like I said, any suggestions/comments would be appreciated, and if this has been done (probably better than I have) if you could direct me to it that would be great.


It, really, seems awfully complicated to me...

The . semantics you propose require to define the "interface" twice:

  • Once for your Proxy
  • Once for the base class

It's a direct violation of DRY, for so little gain!

I don't see much point in using your class over using, simply std::shared_ptr in case of a shared ownership.

There is one reason I myself wrote a pimpl implementation template, and this was for adapting the shared_ptr deleter implementation + deep copying semantics, so as to get value semantics for incomplete types.

Adding layers of helpers in the code ends up making it more difficult to browse.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜