Why use interface instead of pimpl

| category: Programming | author: st
Tags:

The opaque pointer (pimpl) idiom has been inherited from C language where it is used to encapsulate implementation details. However, both old-school and "modern" C++ dispense you from writing some ugly code, and allow to use interfaces with object factories.

The example of implementation with both pimpl and interface approach is as follows.

pimpl.hpp: pimpl implementation header

#include <string>
#include <memory>

using namespace std;

class book
{
public:
    book(const string& title);
    book(const string& title, const string& author_name, const int page_count);
    // If you use pimpl with unique_ptr, you need to declare a destructor
    // which should be implemented in '*.cpp' file
    ~book();
public:
    // Main interface
    string title() const;
    string author_name() const;
    int page_count() const;
private:
    class book_impl; // Defined and implemented in '*.cpp' file
    unique_ptr<book::book_impl> m_impl; // Opaque pointer
};

using book_ptr = unique_ptr<book>;

pimpl.cpp: implementation

#include "pimpl.hpp"
// Here we include some headers i.e. of third-party libraries
// which are required for implementation only
// Once being published, the 'pimpl.hpp' does not depend on them

class book::book_impl
{
public:
    book_impl(const string& name, const string& author_name, const int page_count)
        : m_title(name), m_author_name(author_name), m_page_count(page_count)
    {}
    // Main interface implementation
    string title() const {
        return m_title;
    }

    string author_name() const {
        return m_author_name;
    }

    int page_count() const {
        return m_page_count;
    }
private:
    string m_title;
    string m_author_name;
    int m_page_count;
};

// Now we need to code the redirection of ALL calls
book::book(const string& title)
    : book(title, "Unknown", 1)
{}

book::book(const string& title, const string& author_name, const int page_count)
    : m_impl(make_unique<book_impl>(title, author_name, page_count))
{}

book::~book()
{}

string book::title() const {
    return m_impl->title();
}

string book::author_name() const {
    return m_impl->author_name();
}

int book::page_count() const {
    return m_impl->page_count();
}

no_pimpl.hpp: interface and factory header

#include <string>
#include <memory>

using namespace std;

class book_intf
{
public: // Main interface
    virtual string title() const = 0;
    virtual string author_name() const = 0;
    virtual int page_count() const = 0;
public:
    virtual ~book_intf() {}
};

using book_intf_ptr = unique_ptr<book_intf>;

class book_factory
{
public:
    static book_intf_ptr create(const string& title);
    static book_intf_ptr create(const string& title, const string& author_name, const int page_count);
};

no_pimpl.cpp: implementation

#include "no_pimpl.hpp"

class book_impl : public book_intf
{
public:
    book_impl(const string& title)
        : book_impl(title, "Unknown", 1)
    {}
    book_impl(const string& title, const string& author_name, const int page_count)
        : m_title(title), m_author_name(author_name), m_page_count(page_count)
    {}
public: // Main interface implementation
    string title() const override {
        return m_title;
    }

    string author_name() const override {
        return m_author_name;
    }

    int page_count() const override {
        return m_page_count;
    }
private:
    string m_title;
    string m_author_name;
    int m_page_count;
};

// Book factories
book_intf_ptr book_factory::create(const string& title) {
    return move(make_unique<book_impl>(title));
}

book_intf_ptr book_factory::create(const string& title, const string& author_name, const int page_count) {
    return move(make_unique<book_impl>(title, author_name, page_count));
}

As you can see, redirection of calls is not required when using interfaces, so you do not need to define functions signatures twice with copy-past coding.

Running both:

#include "pimpl.hpp"
#include "no_pimpl.hpp"
#include <memory>
#include <iostream>

int main(int, char**) {
     auto book1 = std::make_unique<book>("Modern old-school C++", "Gang of thousands", 255);
     cout << "Title: " << book1->title()
         << "\nAuthor: " << book1->author_name()
         << "\nPages: " << book1->page_count()
         << endl;

    book_intf_ptr book2 = book_factory::create("Modern old-school C++", "Gang of thousands", 255);
    cout << "Title: " << book2->title()
        << "\nAuthor: " << book2->author_name()
        << "\nPages: " << book2->page_count()
        << endl;
    return 0;
}

About factories

We can define factories at least several different ways:

  • separate factory class with static methods
  • separate factory class with non-static methods (need when additional initializations/settings are required before create an instance)
  • static method within interface class (may be a small harm when derive a new interface from this one)
  • namespace functions (simplest but minimally abstract)