Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow users to include class without including headers used for privates

Tags:

c++

c++11

Sorry for the long and confusing title.

I have a class header file like this

#pragma once
#include <thread>
#include <boost/asio.hpp>
#include <another3rdpartylib/doodads.h>

class A {
public:
  A();
  Method1();
  Method2();
private:
  std::thread thread;
  boost::asio::socket socket;
  another3dpartylib::doodad gizmo;
}

Now the users of the class don't and shouldn't care about the private parts. How can I allow the users to include the class without dragging <thread>, <boost/asio.hpp> and <another3rdpartylib/doodads.h>?

Technically the only thing the users should care about is sizeof(A). Am I mistaken?

like image 499
TheTeaMan Avatar asked Dec 27 '13 16:12

TheTeaMan


People also ask

Why do we declare a class private member variables in a header file?

The primary reason this is needed is that any code that uses a class needs to know about private class members in order to generate code that can handle it.

What are private headers?

Public headers are those needed by the users of the library. Private headers are those needed to compile the library, but which are not needed by library users.

Can a class member function be private?

Private: The class members declared as private can be accessed only by the functions inside the class. They are not allowed to be accessed directly by any object or function outside the class. Only the member functions or the friend functions are allowed to access the private data members of a class.

Why would a member function be declared private?

Private members of a class are still members of the class, so they must be declared, as the implementation of other public members might depend on that private method. Declaring them will allow the compiler to understand a call to that function as a member function call.


2 Answers

The common way in C++ to split the interface and implementation of a class is to use the Pointer To Implementation (PIMPL) Idiom.

The PIMPL idiom encapsulates the implementation of a class by storing a reference/pointer to a class which is in charge of doing the things, only offering the user a class, the interface, which only acts as a wrapper of the implementation class.

For example: Consider a library which implements a extremely fast stack for floating-point operations. The interface of the container is very simple:

class fast_stack
{
public:
    void push( float );
    float pop();
};

But as this library implements a extremely fast stack, its implementation is based on extremely complicated libraries, inline assembly, brainfuck interoperability etc.

The user of that library only want's a stack, not a horrible bunch of code, libraries, and dependencies. How we could hide all of that scream and only provide him a simple and clean interface? Thats where the PIMPL kicks in:

//stack_inferno.hpp

#include <thread>
#include <Boost/ASIO>
... More monters here

class fast_stack_infernal_implementation
{
   std::thread* _mptr_thread_lcl;
   float******* _suicide_cache_memory_pool;
   ... etc etc

   void push( float elem )
   {
        //Please don't see this code, it could hurt your eyes
   }

   float pop()
   {
        // Same as above
   }
};



//fast_stack.hpp (Revisited)

class fast_stack_infernal_implementation; //Note the forward declaration. 

class fast_stack
{
public:
    void push( float );
    float pop();

private:
    std::unique_ptr<fast_stack_infernal_implementation> implm;
};



//fast_stack.cpp

#include "stack_inferno.hpp" //Tah daah!


fast_stack::fast_stack() : impl( new fast_stack_infernal_implementation() )
{
}

void fast_stack::push( float elem )
{
     implm->push( elem );
}

float fast_stack::push()
{
     return implm->pop();
}

As you can see, the PIMPL idiom has many advantages:

  • Hides the complexity of an implementation to the user, and provides only the interface.
  • The user header file (The interface header file) does not include the implementation header, so a change on the implementation does not result in a recompilation of the hole library, and the user code. This is a very important point for a library, because changes on the library not always breaks the user code.
like image 57
Manu343726 Avatar answered Oct 04 '22 17:10

Manu343726


As answered, the basic idea is to isolate the dependencies solely in the .cpp file. This has several implications:

  1. The class size needs not depend on the implementation; a common trick is to use a pointer to a dynamically allocated structure but you can also just reserve raw memory within the class.
  2. Since the class definition does not precise what it is composed of, no method depending on the exact innards of the class may be inlined in the header. This is a penalty one should be aware of.
  3. A benefit of this, though, is that one can maintain ABI compatibility when changing the (hidden) attributes (to some degree).

So, how to apply it ?


The bona-fide PIMPL (templatized):

template <typename T>
class pimpl_internal_interface {
public:
    virtual ~pimpl_internal_interface() {}

    virtual std::unique_ptr<pimpl_internal_interface> clone() const = 0;

    virtual T& get() = 0;
    virtual T const& get() const = 0;
}; // class pimpl_internal_interface

template <typename T>
class pimpl_internal: public pimpl_internal_interface
public:
    template <typename... Args>
    pimpl_internal(Args&&... args): _impl(std::forward<Args>(args)...) {}

    virtual std::unique_ptr<pimpl_internal_interface> clone() const {
        return std::make_unique<pimpl_internal>(_impl);
    }

    virtual T& get() { return _impl; }
    virtual T const& get() const { return _impl; }

private:
    T _impl;
}; // class pimpl_internal

template <typename T>
class pimpl {
public:
    template <typename... Args>
    pimpl(Args&&... args):
       _impl(std::make_unique<pimpl_internal<T>>{std::forward<Args>(args)...}) {}

    pimpl(pimpl&& other) = default;

    pimpl(pimpl const& other): _impl(other->clone()) {}

    pimpl& operator=(pimpl other) {
        std::swap(_impl, other._impl);
        return *this;
    }

    T& get() { return _impl->get(); }
    T const& get() const { return _impl->get(); }
private:
    std::unique_ptr<pimpl_internal_interface<T>> _impl;
}; // class pimpl

Can now be used:

// Complicated.hpp
#pragma once
#include <utils/pimpl.hpp>

class Complicated {
public:
    Complicated();

    void doit();

private:
    struct Impl;
    pimpl<Impl> _;
};

// Complicated.cpp
#include "Complicated.hpp"

// other includes

struct Complicated::Impl {
    // gross stuff you'd rather hide
}; // struct Complicated::Impl

Complicated::Complicated(): _(/*arguments*/) {}

void Complicated::doit() {
    Impl& impl = _.get();
    // use impl
};

The net disadvantage is that it requires a separate dynamic allocation (managed by std::unique_ptr), however it is relatively simple otherwise.


Alternatives ? A common, though a tad more complicated, alternative is to allocate the required memory inline (within the object) to avoid any dynamic allocation. Of course this requires explicitly specifying how much memory is necessary.

template <typename T>
class pimpl_internal_interface {
public:
    virtual void copy(T* dst, T const* src) const = 0;
    virtual void move(T* dst, T* src) const = 0;

    virtual void copy_assign(T* dst, T const* src) const = 0;
    virtual void move_assign(T* dst, T* src) const = 0;

    virtual void destroy(T* t) const = 0;

protected:
    ~pimpl_internal_interface() {}
}; // class pimpl_internal_interface

template <typename T>
class pimpl_internal final: public pimpl_internal_interface<T> {
public:
    virtual void copy(T* dst, T const* src) const override {
        new (dst) T{*src};
    }

    virtual void move(T* dst, T* src) const override {
        new (dst) T{std::move(*src)};
    }

    virtual void copy_assign(T* dst, T const* src) const override {
        *dst = *src;
    }

    virtual void move_assign(T* dst, T* src) const override  {
        *dst = std::move(*src);
    }

    virtual void destroy(T* t) const override {
        t.~T();
    }
}; // class pimpl_internal

template <typename T, size_t Size, size_t Alignment = alignof(void*)>
class pimpl {
public:
    template <typename... Args>
    pimpl(Args&&... args) {
        static_assert(Size >= sizeof(T), "Review Size!");
        static_assert(Alignment >= alignof(T), "Review Alignment!");

        static pimpl_internal<T> const I;
        interface = &I;

        new (&storage) T{std::forward<Args>(args)...};
    }

    pimpl(pimpl&& other): interface(other.interface) {
        interface->move(this->pointer(), other->pointer());
    }

    pimpl(pimpl const& other): interface(other.interface) {
        interface->copy(this->pointer(), other->pointer());
    }

    pimpl& operator=(pimpl&& other) {
        interface->move_assign(this->pointer(), other->pointer());
        return *this;
    }

    pimpl& operator=(pimpl const& other) {
        interface->copy_assign(this->pointer(), other->pointer());
        return *this;
    }

    ~pimpl() { interface->destroy(this->pointer()); }

    T& get() { return *this->pointer(); }
    T const& get() const { return *this->pointer(); }

private:
    using Storage = std::aligned_storage<Size, Alignment>::type;

    T* pointer() { return reinterpret_cast<T*>(&storage); }
    T const* pointer() const { return reinterpret_cast<T const*>(&storage); }

    pimpl_internal_interface<T> const* interface;
    Storage storage;
}; // class pimpl

And using it is similar to the bona-fide version, albeit with an explicit size:

class Complicated {
public:
    Complicated();

    void doit();

private:
    struct Inner;
    pimpl<Inner, 32> _; // reserve 32 bytes for Inner
}; // class Complicated
like image 44
Matthieu M. Avatar answered Oct 04 '22 16:10

Matthieu M.