c++templatesinheritancepolicy-based-design

C++ policy based design: Inheritance vs composition


At Meeting C++ 2019, Jon Kalb gave a talk about template techniques and mentioned policy classes. See here for the source: https://youtu.be/MLV4IVc4SwI?t=1815

The interesting code snippet in question is:

template<class T, class CheckingPolicy>
struct MyContainer : private CheckingPolicy
{
    ...
}

I've seen this type of design quite often and I was wondering if inheritance here has any real advantages over composition. In my personal experience I've heard much about the Prefer composition over inheritance paradigm. So the way I would have written the code would be more like this:

template<class T, class CheckingPolicy>
struct MyContainer
{
    CheckingPolicy policy;
    ...
}

There wouldn't be any virtual functions involved. Nevertheless I'd appreciate it if you could share some insights how these differ. I would be especially interested in differences in memory layout and its implications. Would it make a difference if CheckingPolicy has no data members, but only a check method or an overloaded call operator?


Solution

  • One possible reason: when you inherit from CheckingPolicy, you can benefit from empty base class optimization.

    If CheckingPolicy is empty (i.e. it has no non-static data members other than bit-fields of size 0, no virtual functions, no virtual base classes, and no non-empty base classes), it will not contribute to the size of MyContainer.

    In contrast, when it is a data member of MyContainer, even if CheckingPolicy is empty, size of MyContainer will be increased by at least one byte. At least, because due to alignment requirements you could have additional padding bytes.

    This is the reason why, e.g., in the implementation of std::vector you can find ihnehritance from an allocator. For example, libstdc++'s implementation:

    template<typename _Tp, typename _Alloc>
    struct _Vector_base {
        typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
            rebind<_Tp>::other _Tp_alloc_type;
    
        struct _Vector_impl : public _Tp_alloc_type, public _Vector_impl_data { 
            // ...
        };
    
        // ...
    };
    

    Stateless allocators (like CheckingPolicy with no non-static data members) will not contribute into std::vector's size.

    In C++20 we'll have [[no_unique_address]] to potentially address this issue: whereas empty base optimization is required for standard layout types, [[no_unique_address]] is just a permission, not a requirement. (Thanks Nicol Bolas for pointing that out.)