J
Juha Nieminen
Let me share with you a realization I had about private inheritance.
As we all know, C++ supports three types of inheritance: Public,
private and protected. From these only the public inheritance is a
"true" inheritance from an object-oriented point of view. If you inherit
privately, there is no "is-a" relationship between the derived and the
base classes, and thus private inheritance does not conform to
object-oriented design. So why have this odd private inheritance at all?
Let me present you an example where I found it useful.
Suppose you want to create an intrusive smart pointer which supports
specifying which memory allocator was used to allocate the object being
managed (so that the smart pointer will be able to destroy the object
using that same memory allocator). A naive implementation could look
something like this:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr
{
Obj_t* obj;
Allocator allocator;
public:
// All the necessary public methods here
};
The class has to store the allocator object (which it has to take eg.
as a constructor parameter) because it is possible for allocators to
have an internal state, and if this is so, it must be stored so that the
object can be destroyed appropriately. You cannot simply assume you can
instantiate the allocator whenever it's needed (this could work with
stateless allocators but not with ones with an internal state).
There's an annoying problem with this, though. Most allocators
(including std::allocator) are empty. They do not have any internal
state (ie. they do not have any member variables). If the allocator is
empty, the IntrusivePtr above is reserving completely unused space for
it by keeping an instance as member. The 'allocator' object will take at
least 1 byte (which will then be expanded to the natural word size of
the system for alignment reasons). Thus the size of the IntrusivePtr
class is increased for no good reason: The space is not used for
anything and thus it's a complete waste.
Usually one would want smart pointers (especially intrusive ones) to
be as small as possible. The 'allocator' member effectively doubles the
size of the class, completely uselessly if the allocator is empty.
However, if we want this class to work properly with all possible
allocators, we have to reserve some space for it. However, wouldn't
there be any way of not reserving space for empty allocators?
Well, most C++ compilers implement the so-called empty base class
optimization (allowed by the C++ standard). This means that if we
inherit from an empty base class, the base class will take no space in
the derived class at all. By "abusing" this feature we can make an
improved version of the above class:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr: public Allocator
{
Obj_t* obj;
public:
// All the necessary public methods here
};
Now it's perfect spacewise: If the specified allocator does have an
internal state, it will be stored in the IntrusivePtr class. However,
most importantly, if the allocator is empty, it will take no space at
all (at least with most compilers). The allocator can still be used for
everything that the member could be used by IntrusivePtr.
However, there's a design problem here. By inheriting from Allocator
we are effectively saying that IntrusivePtr is an Allocator. However,
that's not what we meant. We are simply "abusing" inheritance for a
compiler optimization technique, rather than what inheritance is usually
used for in object-oriented programming. This is, basically, a wrong
usage of inheritance. A smart pointer is not an allocator, and shouldn't
be made one. (In this case the proper design is that the smart pointer
*contains* an allocator object, the smart pointer is not an allocator
itself.)
Ok, it's mostly only a cosmetic problem, but it still bothers an
object-oriented purist (like me). Isn't there anything that can be done
about it?
Step in private inheritance:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr: private Allocator
{
Obj_t* obj;
public:
// All the necessary public methods here
};
Now we are inheriting from Allocator, but we are saying that
"IntrusivePtr is *not* an Allocator". We get all the optimization
benefits of the inheritance trick, while still maintaining good
object-oriented design. The IntrusivePtr class can access everything in
Allocator as needed (and if a pointer to the allocator object is needed
for whatever reason, the 'this' pointer can be cast to it), but the
allocator doesn't mess up the public interface of the class.
Basically private inheritance is composition, but getting the
technical benefits of inheritance (in this case empty base class
optimization).
(This is also a good example of where multiple inheritance can be
useful. If, for whatever reason, IntrusivePtr would have to be inherited
from some base class somewhere, we can inherit it both from that base
class and Allocator without problems. This would be a case where the
language supporting multiple inheritance is beneficial.)
Now, if I only could figure out a good example of protected inheritance...
As we all know, C++ supports three types of inheritance: Public,
private and protected. From these only the public inheritance is a
"true" inheritance from an object-oriented point of view. If you inherit
privately, there is no "is-a" relationship between the derived and the
base classes, and thus private inheritance does not conform to
object-oriented design. So why have this odd private inheritance at all?
Let me present you an example where I found it useful.
Suppose you want to create an intrusive smart pointer which supports
specifying which memory allocator was used to allocate the object being
managed (so that the smart pointer will be able to destroy the object
using that same memory allocator). A naive implementation could look
something like this:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr
{
Obj_t* obj;
Allocator allocator;
public:
// All the necessary public methods here
};
The class has to store the allocator object (which it has to take eg.
as a constructor parameter) because it is possible for allocators to
have an internal state, and if this is so, it must be stored so that the
object can be destroyed appropriately. You cannot simply assume you can
instantiate the allocator whenever it's needed (this could work with
stateless allocators but not with ones with an internal state).
There's an annoying problem with this, though. Most allocators
(including std::allocator) are empty. They do not have any internal
state (ie. they do not have any member variables). If the allocator is
empty, the IntrusivePtr above is reserving completely unused space for
it by keeping an instance as member. The 'allocator' object will take at
least 1 byte (which will then be expanded to the natural word size of
the system for alignment reasons). Thus the size of the IntrusivePtr
class is increased for no good reason: The space is not used for
anything and thus it's a complete waste.
Usually one would want smart pointers (especially intrusive ones) to
be as small as possible. The 'allocator' member effectively doubles the
size of the class, completely uselessly if the allocator is empty.
However, if we want this class to work properly with all possible
allocators, we have to reserve some space for it. However, wouldn't
there be any way of not reserving space for empty allocators?
Well, most C++ compilers implement the so-called empty base class
optimization (allowed by the C++ standard). This means that if we
inherit from an empty base class, the base class will take no space in
the derived class at all. By "abusing" this feature we can make an
improved version of the above class:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr: public Allocator
{
Obj_t* obj;
public:
// All the necessary public methods here
};
Now it's perfect spacewise: If the specified allocator does have an
internal state, it will be stored in the IntrusivePtr class. However,
most importantly, if the allocator is empty, it will take no space at
all (at least with most compilers). The allocator can still be used for
everything that the member could be used by IntrusivePtr.
However, there's a design problem here. By inheriting from Allocator
we are effectively saying that IntrusivePtr is an Allocator. However,
that's not what we meant. We are simply "abusing" inheritance for a
compiler optimization technique, rather than what inheritance is usually
used for in object-oriented programming. This is, basically, a wrong
usage of inheritance. A smart pointer is not an allocator, and shouldn't
be made one. (In this case the proper design is that the smart pointer
*contains* an allocator object, the smart pointer is not an allocator
itself.)
Ok, it's mostly only a cosmetic problem, but it still bothers an
object-oriented purist (like me). Isn't there anything that can be done
about it?
Step in private inheritance:
template<typename Obj_t, typename Allocator = std::allocator<Obj_t> >
class IntrusivePtr: private Allocator
{
Obj_t* obj;
public:
// All the necessary public methods here
};
Now we are inheriting from Allocator, but we are saying that
"IntrusivePtr is *not* an Allocator". We get all the optimization
benefits of the inheritance trick, while still maintaining good
object-oriented design. The IntrusivePtr class can access everything in
Allocator as needed (and if a pointer to the allocator object is needed
for whatever reason, the 'this' pointer can be cast to it), but the
allocator doesn't mess up the public interface of the class.
Basically private inheritance is composition, but getting the
technical benefits of inheritance (in this case empty base class
optimization).
(This is also a good example of where multiple inheritance can be
useful. If, for whatever reason, IntrusivePtr would have to be inherited
from some base class somewhere, we can inherit it both from that base
class and Allocator without problems. This would be a case where the
language supporting multiple inheritance is beneficial.)
Now, if I only could figure out a good example of protected inheritance...