Automatic Insulated Implementation

Intro

For non-trivial classes with non-trivial implementations, one often separates the private data members and functions from the interface file (header). The reasons are many:

This is usually implemented by gathering the private members of the class into a nested structure that is defined in the source file for the class. A pointer is kept to the implementation structure.


// interface

    class Foo {
        struct Impl;
        Impl* const impl;
    public:
        Foo();
        Foo( Foo const& );
        ~Foo();
        
        Foo& operator = ( Foo const& );
        
        void update( int, int );
        int check() const;
    };
    
// implementation

    struct Foo::Impl {
        int i, j;
        SomeBigObject obj;
        // ...
    };
    
    Foo::Foo() : impl( new Impl ) { }
    
    Foo::Foo( Foo const& from ) : impl( new Impl( *from.impl ) ) { }
    
    Foo::~Foo() { delete impl; }
    
    Foo& Foo::operator = ( Foo const& from ) {
        *impl = *from.impl;
        return *this;
    }
    
    void Foo::update( int i, int j ) {
        impl->i = i;
        impl->j = j;
    }
    
    int Foo::check() const {
        impl->i = 0; // allowed
        return impl->i * impl->j;
    }

Making it convenient and safe

This gets the job done, but not without unnecessary glue and room for error. Foo must have all four members implemented (normal ctor, copy ctor, copy assign, destructor). If any are forgotten, there will likely *not* be any compile-time error. Const-ness of the Foo object is not propagated, so, for example, in check(), the implementation can be modified. If an exception occurs in one of Foo's constructors, the implementation object won't be deleted. Some of these problems can be solved by using std::auto_ptr in place of the raw pointer, but this adds problems of its own, as std::auto_ptr is not specialized for this particular use.

This idiom can be encapsulated in a class that eliminates all this mess.


// library

    template<class T>
    class auto_impl {
        T* const impl;
    public:
        auto_impl() : impl( new T ) { }
        ~auto_impl() { delete impl; }
        
        auto_impl( auto_impl const& from ) : impl( new T( *from.impl ) ) { }
        
        auto_impl& operator = ( auto_impl const& from ) {
            *impl = *from.impl;
            return *this;
        }
        
        operator T      * ()          { return impl; }
        operator T const* () const    { return impl; }
        
        T      * operator -> ()       { return impl; }
        T const* operator -> () const { return impl; }
    };

// interface

    class Foo {
        struct Impl;
        auto_impl<Impl> impl;
    public:
        Foo();
        Foo( Foo const& );
        ~Foo();
        
        Foo& operator = ( Foo const& );
        
        void update( int, int );
        int check() const;
    };
    
// implementation

    struct Foo::Impl {
        int i, j;
        SomeBigObject obj;
        // ...
    };
    
    Foo::Foo() { }
    
    Foo::Foo( Foo const& from ) : impl( from.impl ) { }
    
    Foo::~Foo() { }
    
    Foo& Foo::operator = ( Foo const& from ) {
        impl = from.impl;
        return *this;
    }
    
    void Foo::update( int i, int j ) {
        impl->i = i;
        impl->j = j;
    }
    
    int Foo::check() const {
        impl->i = 0; // correctly flagged as error
        return impl->i * impl->j;
    }

Note that Foo's copy ctor and copy assign have to be implemented in the source file, because that is the only place the definition of struct Foo::Impl is known. If those aren't needed, they don't need to be provided:


// interface

    class Foo {
        struct Impl;
        auto_impl<Impl> impl;
    public:
        Foo();
        ~Foo();
        // ...
    };
    
// implementation

    struct Foo::Impl {
        // ...
    };
    
    Foo::Foo() { }
    
    Foo::~Foo() { }

If a user tries to copy or assign a Foo, there will be an error instantiating auto_impl<Impl>, unlike the original, where the compiler-generated copy ctor of copy assign would be used, producing bad results.

If some data members do need to be modified in const methods, they can be made mutable, as always.

Details

What if ctor initializers are needed for the implementation members? Constructor arguments can be forwarded from the interface's constructor to the implementation's constructor. This can be achieved with a (messy) forwarding function in auto_impl,


// library

    template<class T>
    class auto_impl {
    public:
        // ...
        template<typename A1>
        inline explicit auto_impl( A1& a1 ) : impl( new T( a1 ) ) { }
        
        // additional forwarding ctors for 2, 3, 4, etc. args
        
        // ...
    };

or by having the user allocate the implementation object when they need to provide it with constructor parameters


// library

    template<class T>
    class auto_impl {
    public:
        // ...
        
        // user *must* allocate with a form of new whose result is legal
        // for a delete expression
        auto_impl( T* t ) : impl( t ) { }
        // ...
    };
    
// implementation

    struct Foo::Impl {
        // ...
        Impl( int );
    };
    
    Foo::Foo() : impl( new Impl( 1 ) ) { }
    
    Foo::~Foo() { }

Inside member functions, using the impl-> prefix can become tedious. This can be eliminated by adding an inline helper function to the implementation class:


    struct Foo::Impl {
        inline int check() const {
            return i * j;
        }
    };
    
    int Foo::check() const {
        return impl->check();
    }

Optimization

To prevent multiple reads of the pointer to the implementation struct, the above technique can be used, or the impl pointer can be explicitly locally cached:


    int Foo::check() const {
        Impl const* const impl = this->impl; // cache
        
        return impl->i * impl->j;
    }

If the freestore allocation of the implementation is too expensive, it can be allocated with a custom allocator.


    // in some library
    class fast_alloc {
        // ...
        void* operator new ( std::size_t );
        void operator delete ( void* );
    };

    struct Foo::Impl : fast_alloc {
        // ...
    };

If this isn't enough, the allocation can be eliminated completely by introducing a size dependency in the interface:


    // ensures alignment for any type
    struct max_align {
        long l;
        long double d;
        void* p;
        void (*f)();
        void (max_align::*mf)();
        // etc.
    };

    template<class T,class alloc_size>
    class auto_impl_contained {
        union {
            max_align align;
            alloc_size alloc;
        } storage;
    public:
        auto_impl_contained() {
            ::new (static_cast<void*> (&storage)) T;
        }

        ~auto_impl_contained() {
            operator T* ()->T::~T(); // destruct object
        }

        operator T* () {
            return static_cast<T*> (static_cast<void*> (&storage));
        }

        // ...
    };

    class Foo {
        struct Impl;
        auto_impl_contained<Impl,void* [10]> impl;
    // ...
    };

The size is specified as a type instead of as std::size_t because it is easier to estimate the storage required for a type as a number of void* (or some other type) instead of chars. If one knows the size in chars (bytes), it can be specified as a char array:


    auto_impl_contained<T,char [n]>

Blargg's C++ Notes