Is there a way to combine the benefits of compiler

2020-06-21 05:11发布

问题:

Suppose I have a class with a private member, which is an implementation detail that clients of the class don't care about. This class is a value type and we want it to be copyable, eg

#include <boost/bimap.hpp>  // some header that pulls in many other files

class MyClass {
public:
    MyClass() {}
    ...
private:
    boost::bimap<Key,Value>   table;
};

Now every client of MyClass is forced to pull in lots of boost headers it doesn't really need, increasing build times. However, the class is at least copyable.

If we introduce a compiler firewall (Pimpl idiom) then we can move the #include dependency to the cpp file, but now we have to do lots more hard work due to the Rule of 5:

// no extra #includes - nice
class MyClass {
public:
    MyClass() {}
    // ugh, I don't want this, just make it copyable!
    MyClass(const MyClass& rhs);
    MyClass(MyClass&& rhs);
    MyClass& operator=(const MyClass& rhs);
    MyClass& operator=(MyClass&& rhs);
    ~MyClass() {}
    ...
private:
    std::unique_ptr<MyClassImpl>  impl;
};

Is there a technique for getting the benefits of the compiler firewall, but retaining copyability so that I don't need to include the Rule of 5 boilerplate?

回答1:

I think the best solution here is to build your own deep-copying smart pointer. If you tailor it to storing Pimpls only, it shouldn't be too difficult:

template <class P>
class pimpl_ptr
{
  std::unique_ptr<P> p_;

public:
  ~pimpl_ptr() = default;

  pimpl_ptr(const pimpl_ptr &src) : p_(new P(*src.p_)) {}
  pimpl_ptr(pimpl_ptr &&) = default;
  pimpl_ptr& operator= (const pimpl_ptr &src) { p_.reset(new P(*src.p_)); return *this; }
  pimpl_ptr& operator= (pimpl_ptr &&) = default;

  pimpl_ptr() = default;
  pimpl_ptr(P *p) : p_(p) {}

  P& operator* () const { return *p_; }
  P* operator-> () const { return &**this; }

  // other ctors and functions as deemed appropriate
};

Just document that it doesn't support pointing to base class subobjects, and you're set. You could enforce this by not giving it a constructor taking a pointer, and enforcing construction through make_pimpl:

template <class P>
class pimpl_ptr
{
  // as above, P* ctor is private
private:
  pimpl_ptr(P *p) : p_(p) {}

  template <class T, class... Arg>
  friend pimpl_ptr<T> make_pimpl(Arg&&... arg);
};

template <class T, class... Arg>
pimpl_ptr<T> make_pimpl(Arg&&... arg)
{
  return pimpl_ptr<T>(new T(std::forward<Arg>(arg)...));
}