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?
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
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With