keeping private parts outside c++ headers: pure virtual base class vs pimpl

梦想与她 提交于 2019-12-12 07:58:31

问题


I recently switched back from Java and Ruby to C++, and much to my surprise I have to recompile files that use the public interface when I change the method signature of a private method, because also the private parts are in the .h file.

I quickly came up with a solution that is, I guess, typical for a Java programmer: interfaces (= pure virtual base classes). For example:

BananaTree.h:

class Banana;

class BananaTree
{
public:
  virtual Banana* getBanana(std::string const& name) = 0;

  static BananaTree* create(std::string const& name);
};

BananaTree.cpp:

class BananaTreeImpl : public BananaTree
{
private:
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

public:
  BananaTreeImpl(string name) 
    : name(name)
  {}

  virtual Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }
};

BananaTree* BananaTree::create(string const& name)
{
  return new BananaTreeImpl(name);
}

The only hassle here, is that I can't use new, and must instead call BananaTree::create(). I do not think that that is really an problem, especially since I expect to be using factories a lot anyway.

Now, the wise men of C++ fame, however, came up with another solution, the pImpl idiom. With that, if I understand it correctly, my code would look like:

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

BananaTree.cpp:

struct BananaTree::Impl
{
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

  Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }

  Impl(string const& name) : name(name) {}
}

BananaTree::BananaTree(string const& name)
  : pimpl_(shared_ptr<Impl>(new Impl(name)))
{}

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->getBanana(name);
}

This would mean I have to implement a decorator-style forwarding method for every public method of BananaTree, in this case getBanana. This sounds like an added level of complexity and maintenance effort that I prefer not to require.

So, now for the question: What is wrong with the pure virtual class approach? Why is the pImpl approach so much better documented? Did I miss anything?


回答1:


I can think of a few differences:

With the virtual base class you break some of the semantics people expect from well-behaved C++ classes:

I would expect (or require, even) the class to be instantiated on the stack, like this:

BananaTree myTree("somename");

otherwise, I lose RAII, and I have to manually start tracking allocations, which leads to a lot of headaches and memory leaks.

I also expect that to copy the class, I can simply do this

BananaTree tree2 = mytree;

unless of course, copying is disallowed by marking the copy constructor private, in which case that line won't even compile.

In the above cases, we obviously have the problem that your interface class doesn't really have meaningful constructors. But if I tried to use code such as the above examples, I'd also run afoul of a lot of slicing issues. With polymorphic objects, you're generally required to hold pointers or references to the objects, to prevent slicing. As in my first point, this is generally not desirable, and makes memory management much harder.

Will a reader of your code understand that a BananaTree basically doesn't work, that he has to use BananaTree* or BananaTree& instead?

Basically, your interface just doesn't play that well with modern C++, where we prefer to

  • avoid pointers as much as possible, and
  • stack-allocate all objects to benefit form automatic lifetime management.

By the way, your virtual base class forgot the virtual destructor. That's a clear bug.

Finally, a simpler variant of pimpl that I sometimes use to cut down on the amount of boilerplate code is to give the "outer" object access to the data members of the inner object, so you avoid duplicating the interface. Either a function on the outer object just accesses the data it needs from the inner object directly, or it calls a helper function on the inner object, which has no equivalent on the outer object.

In your example, you could remove the function and Impl::getBanana, and instead implement BananaTree::getBanana like this:

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}

then you only have to implement one getBanana function (in the BananaTree class), and one findBanana function (in the Impl class).




回答2:


Actually, this is just a design decision to make. And even if you make the "wrong" decision, it's not that hard to switch.

pimpl is also used to provide ligthweight objects on stack or to present "copies" by referencing to the same implementation object.
The delegation-functions can be a hassle, but it's a minor issue (simple, so no real added complexity), especially with limited classes.

interfaces in C++ are typically more used in strategy-like ways where you expect to be able to choose implementations, although that is not required.



来源:https://stackoverflow.com/questions/3092444/keeping-private-parts-outside-c-headers-pure-virtual-base-class-vs-pimpl

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!