Is there a way to combine the benefits of compiler firewalls (Pimpl) and default-copyability?
Asked Answered
P

1

7

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?

Palate answered 15/10, 2014 at 8:50 Comment(1)
C
9

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)...));
}
Cerelia answered 15/10, 2014 at 9:0 Comment(2)
Good technique: I guess you could combine with type traits to enforce that it can only be used with copyable value types (though including boost/type_traits would lead us back to square one in terms of unwanted dependendencies...)Palate
~pimpl_ptr() = default; causes undefined behavior because the template type P has a non-trivial destructor which isn't visible to the compilerTurbulence

© 2022 - 2024 — McMap. All rights reserved.