Pointer to implementation is one of the idioms in C++ which dictates that the implementation must be accessed through an indirection, which in simple words mean that we encapsulate all the member data of our class in another struct that we access via a pointer.
Benefits
The benefits comes in the form of lowered compilation times, easier to manage headers, the reason compile times decrease is because instead of declaring every member in a header file which than forces us to add headers as well, for example if we have members of type vector and string we will have to include their header files as well and every time we would make a change in those data member types or Let’s say we have another custom type which gets new revisions very often that would result in us recompiling the entire header for our class again.
Use Pointers
Like the name says we hold a pointer to implementation, the pointer can be a raw pointer, unique pointer or a shared pointer.
Raw and unique pointers are most performance friendly so we will look into those first.
Pointer
// Header file
class Widget
{
public:
Widget();
~Widget();
private:
struct Impl; // Also known as forward declaring or incomplete type
Impl* pImpl; // Pointer to implementation
}
// Source file
struct Widget::Impl
{
std::vector<int> m_age;
std::string m_name;
PlayerController m_controller; // Our custom type
}
Widget::Widget()
{
// Constuctor
pImpl = new Impl();
}
Widget::~Widget()
{
delete pImpl;
}
This is straight forward, but we are here for smart pointers aren’t we? Let's see how this works with a unique pointer:
Unique Pointer
Before we dive into code lets establish some base points regarding pointer to implementation idiom.
- It hides private data, so the API remains stable.
- It won’t recompile the header as it just holds a 4 byte (OS dependent) pointer to implementation.
- Downfall is the introduction of an indirection as now you have a pointer.
- Beware while creating a copy constructor.
- This is well used for classes that have huge number of private members.
// Header file
class Person
{
public:
Person(std::string name, std::string address);
~Person();
// Copy operations
Person(const Person& rhs);
Person& operator=(const Person& rhs) = delete;
// Move operations
Person(Person&& rhs);
Person& operator=(Person&& rhs) = delete;
std::string GetAttributes() const;
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
}
// Source file
// Always first define the structure where member data is stored
struct Person::Impl
{
std::string name;
std::string address;
};
Person::Person(std::name, std::address) : pImpl{std::make_unique<Impl>()}
{
pImpl->name = name;
pImpl->address = address;
}
Person::~Person() = default;
Person::Person(const Person& rhs)
{
pImpl = std::make_unique<Impl>();
pImpl->name = rhs.pImpl->name;
pImpl->address = rhs.pImpl->address;
}
Person::Person(Person&& rhs) noexcept
{
pImpl = std::move(rhs.pImpl);
}
std::string Person::GetAttributes() const
{
return pImpl->name + '\t' + pImpl->address;
}
A few things to note:
- Always define the implementation struct before you call the constructors of class else compiler will throw undefined errors because unique pointers don’t work with incomplete types as they needed to be defined along with destructors ahead of usage.
- Remember the special member function generation rules, if you declare a destructor then it will prevent compiler from auto generating the move operations.
- With unique pointer you have to define your own copy constructor else it will do a shallow copy.
Shared Pointer
Though using shared pointers is not common but with shared pointers you don’t have a problem with incomplete type neither do you have to custom define any move or copy operation, with shared pointer we will have no effect as they are copyable and moveable by nature.
Top comments (0)