Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any way to limit repetitive boilerplate when using the PIMPL idiom?

I have something like the following:

// foo.h:
class foo {
public:
    foo(); 
    ~foo();
    // note: the param type repetition here is only incidental, assume the
    // functions can't easily be made to share type signatures
    void bar(a b, c d);               
    void baz(a b, c d, e f, g h, i j);
    void quux(a b, c d, e f, g h, i j);
private:
    class impl;
    impl *m_pimpl;
}

Then:

// foo.cpp:
class foo::impl {
public:
    void bar(a b, c d);
    void baz(a b, c d, e f, g h, i j);
    void quux(a b, c d, e f, g h, i j);

private:
    // lots of private state, helper functions, etc. 
};

void foo::impl::bar(a b, c d) { ... }
void foo::impl::baz(a b, c d, e f, g h, i j) { ... }
void foo::impl::quux(a b, c d, e f, g h, i j) { ... }

foo::foo() : m_pimpl(new impl()) { }
foo::~foo() { delete m_pimpl; m_pimpl = NULL; }
void foo::bar(a b, c d) {
    return m_pimpl->bar(b, d);
}
void foo::baz(a b, c d, e f, g h, i j) {
    return m_pimpl->baz(b, d, f, h, j)
}
void foo::quux(a b, c d, e f, g h, i j) {
    return m_pimpl->quux(b, d, f, h, j);
}

There's a lot of repetition of bar, baz, and quux here:

  • Once in foo's declaration
  • Once in foo::impl's declaration
  • Once in foo::impl's definitions
  • Twice in foo's definitions: once for foo's function param list and again to call the corresponding foo::impl functions.

For each of these except the last I have to write out the entire parameter list, whose types can be more involved.

What's the best way, if any, to reduce the repetition here? One easy one is to inline the definition of the foo::impl public functions, but is there anything besides that outside of designing the classes differently to have fewer public functions?

like image 713
Claudiu Avatar asked Apr 10 '15 22:04

Claudiu


2 Answers

Using signatures, we can reduce it to 3 mentions, plus 1 set of forwards:

using signature_1 = int(int);

struct foo {
  signature_1 x;
  foo();
  ~foo();
private:
  struct fooimpl;
  std::unique_ptr<fooimpl> pimpl;
};

int main() {
  foo f;
  std::cout << f.x(1) << '\n';
}
struct foo::fooimpl {
  signature_1 x;
  int v = 3;
};

foo::~foo() = default;
foo::foo():pimpl(new foo::fooimpl()){}

int foo::x(int y){return pimpl->x(y);}
int foo::fooimpl::x(int y){return y+v;}

We can get it down to 2+forward if our pimpl is a pure virtual class. Write a map from decltype(&foo::method)->signature, and have the pure virtual interface use that (and decltype) to invent the signature of the pure virtual class.

Write the signature once in foo, decltype-determine it in foo_impl_interface, and then implement it inline within foo_impl in the cpp file. Plus one set of forwards.

 template<class MethodPtrType>
 struct method_sig;
 template<class MethodPtrType>
 using method_sig_t = typename method_sig<MethodPtrType>::type;
 template<class T, class R, class...Args>
 struct method_sig< R(T::*)(Args...) > {
   using type=R(Args...);
 };

plus another 11 odd specializations (sigh) to get all the cases. (const& const const&& const volatile const volatile& const volatile&& & && volatile volatile& volatile&& qualifiers are all, as far as I can tell, required).

Now method_sig_t<decltype(&foo::x)> is int(int), which we can use:

struct foo_impl_interface {
  virtual method_sig_t<decltype(&foo::x)> x = 0;
  virtual ~foo_impl_interface() {}
};

assuming we are using pimpl to regularize our type rather than hide the state.


Finally, instead of implementing most of your code in the pimpl, instead just store STATE in the pimpl, leave the code in the class itself.

This gives you the "I others do not depend on my size": who cares if the code is in foo or foo_impl that reads foo_impl's state? So if you aren't doing the foo_impl_interface technique, why forward?

like image 91
Yakk - Adam Nevraumont Avatar answered Nov 14 '22 21:11

Yakk - Adam Nevraumont


If your public class just proxies calls to the implementation, consider using interface -> implementation model instead of pimpl.

You would reference your implementation only by the interface in the user code with class foo being interface

class foo {
public:
    virtual void bar(a b, c d) = 0;               
    virtual void baz(a b, c d, e f, g h, i j) = 0;
    virtual void quux(a b, c d, e f, g h, i j) = 0;
    virtual ~foo(){}
}

Idea behind pimpl is to store private data and functions in the separate object pointer so you don't break your public class interface in case of data storage and handling change. But it does not imply all code moved to the private object. So you usually implement your public functions just in place. You wouldn't break user code when your public functions implementation change, as your public functions implementation will be hiden from the user along with private class definition.

like image 43
Lol4t0 Avatar answered Nov 14 '22 22:11

Lol4t0