The C++ Priv idiom: an alternative to Pimpl 2021.03.09

C++ is infamous for long compilation times.

Part of the problem is that editing private declarations of a class causes recompilation for all of its users.

There are several ways to work around this problem and reduce incremental compile times:

Below is a short introduction to Pimpl and Priv, some benchmark results, and closing commentary.

Illustrating Pimpl with a simple example

class SpamFilter
{
public:
    SpamFilter (const std::set<std::string>& forbiddenWords);
    ~SpamFilter();

    bool isSpam (const std::vector<std::string>& words) const;

private:
    class Impl;

    std::unique_ptr<Impl> impl;
};

Using the Pimpl idiom, our class doesn't have any private declarations apart from the internal Impl class and impl member.

The actual SpamFilter::Impl class is defined in the .cpp file and does all of the work.

The benefit of this approach is that when we change implementation details, no recompilation of other modules will trigger.

The down-sides are:

Priv

Priv is an alternative idiom without the down-sides of Pimpl, but also with only a part of its benefit, as we do declare the private members normally:

class SpamFilter
{
public:
    SpamFilter (const std::set<std::string>& forbiddenWords);
    bool isSpam (const std::vector<std::string>& words) const;

private:
    struct Priv;

    std::set<std::string> m_forbiddenWords;
};

No private methods are declared in the .h file. According to this idiom they all belong to the Priv subclass, declared in the .cpp file:

struct SpamFilter::Priv
{
    static bool isSpam (const SpamFilter&, const std::string& word);
};

The private methods turned into static methods of the Priv nested class, so we use them like so:

bool SpamFilter::isSpam (const std::vector<std::string>& words) const
{
    for (const auto& x : words)
        if (Priv::isSpam (*this, x))
            return true;
    return false;
}

Comparison of idioms

Boilerplate Slowdown Extra Recompilations
Plain None (baseline) On any change
Interface +37% less than 1% When the interface changes
Pimpl +53% 10%
Priv +13% None Also when private members change

The comparison numbers are based on a simple benchmark (run-time was measured using time on a 2020 M1 Macbook Pro)

Should one use any of these idioms just to reduce compilation times? As C++ already has plenty of boiler-plate with header files, I'd be relunctant to add more. And why are we even using C++ in the first place if not to achieve the best run-time performance? Therefore I would prefer not to use the Pimpl idiom, and consider Priv over it, but also after exausting any other approaches to reduce compilation times without any weird workaround (like using forward declarations and include-what-you-use).

Notes

pajam!