Pimpl and potential alternatives

Started by
10 comments, last by Hodgman 8 years, 8 months ago

Hi,

so recently I was starting out on a new simple project in C++ and I was thinking about an overall structure on how to organize my code in a proper way.

At this point modules came to my mind. The main point here is that I don't know how to properly seperate the implementation from the interface so that no implementation details of module Y are exposed to module X that is using module Y. Unfortunately C++ does not support modules in a native way because header files do serve as a class definition and interface at the same time...

So I already read about the pimpl idiom and I can see its advantages but there are also some drawbacks in using it (e.g. dynamic memory allocation).

All this is leading to the important question(s):

How do you handle this topic within your projects?

Do you know some techniques beside pimpl to achieve a clean separation between the interface and the implementation? If yes, how do they work and are they field-proven?

Do you know of some open source code that handles this topic quite well? (for further investigation)

Thank you in advance.

Advertisement


How do you handle this topic within your projects?

I don't worry about it at all. Private variables being visible in headers is no different than a module-based languages such as Java, where *everything* is in one file.

The only place that this matters is when exporting an API for dynamic linking. There you almost always want to use the PIMPL idiom (but only for classes in the public API, because it prevents the consumer of your API for being dependent on the actual size of each class (hence allowing you to add/remove private variables without breaking the API contract).

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

I used to use pImpl in my project, but I've since decided it wasn't worth bothering with, and gradually (as needed) removed most of them.

If doing it for compile times, just forward-declare anything you can. If doing it to "black box" your implementation, I've never really had a problem with that since others aren't using my libraries... and if they were using my libraries, I'm not Microsoft whose business depends on backwards compatibility, so I wouldn't care about breaking someone's build if they knowingly did something stupid.

Qt (opensource, well-designed, widely used, backwards-compatibility is important) uses pImpl or pImpl-like mechanisms. It just makes it harder to read for an outsider like me trying to locate and report bugs in their code. Their code is consistently written and they stick to their chosen coding principles (at least it seems so, what few times I've dived into it), but until I bother to get used to it, it looks very messy.

I'm confident I could find my way around Qt's internals and be at home within a week or two's time, but I have my own projects to work on, so all it does it reduce the likelyhood of me finding and reporting bugs, fixing them and submitting the patches (because of redirections with things like pImpls, one-letter abbreviations, and not enough (IMO) internal commenting explaining why not what, and hiding logic behind macroes).

Library writers have two conflicting goals: they want to hide the implementation from the user, but they also want to make the implementation easy to understand for maintainers - and the maintainers are users.

For a small in-house library, or one that you're sharing publicly but not financially-tied to, I'd just trust the programmer to not abuse the knowledge of the internals, and if he does, it'd be quite charitable of you to further his education by letting him get burned by it. tongue.png

Don't forget good ol' abstract interfaces.

They're generally a terrible thing to use for simple objects, but for larger systems (e.g. high-level audio, input, rendering, etc.) they are a very good tool. You can expose an IAudioSystem abstract interface in a public header and then provide an implementation in a private header and source file. Most of your code only needs to include the IAudioSystem header and call virtual methods.

You can also use static interface, but on PC hardware there's little reason to bother. Unless you're planning to target in-order architectures with crappy branch prediction (e.g. XBox360, PS4, etc.) there's no good reason to avoid virtual functions in high-level code (though of course you wouldn't want to go near them in code used in tight inner loops, like in math kernels).

Sean Middleditch – Game Systems Engineer – Join my team!

NVI is a better alternative to PIMPL much of the time.

NVI allows the explicit separation of your public interface (which is public and non-virtual) from your private/implementation interface (which is private, and possibly virtual). It ends up accomplishing the same thing as the PIMPL but without a lot of code duplication and with more explicit self-documentation on functional variance across derived classes.

The only advantage of PIMPL of NVI is you can hide ABI changes behind PIMPL.

Stephen M. Webb
Professional Free Software Developer

Thank you all for your replies.

NVI is a better alternative to PIMPL much of the time.

I will take a closer look at this option.

Based on your link this seems to work quite good for a log system (though a pretty simple one):

Logger.h:


class Logger {
public:
    void log(const std::string& message) {
        // get the time etc.
        auto completeLogMessage = /* build one */
        log_impl(completeLogMessage);
    }

private:
    virtual void log_impl(const std::string& completeLogMessage) = 0;
};

CommandLineLogger:


class CommandLineLogger : public Logger {
private:
    virtual void log_impl(const std::string& completeLogMessage) {
        std::cout << completeLogMessage << std::endl;
    }
}

It would also be possible to add a specific implementation that is logging to a database etc. which is pretty cool.

This surely does not represent the full potential of this idiom but I will keep this in my mind and try to apply it when dealing with a more complex scenario.

@SeanMiddleditch:

This is the way my prototype is working at the moment since I didn't trust the pimpl tongue.png

@Servant of the Lord:

Thanks for pointing out Qt - I will have a look at the source code so some things might get more clear when seen in production code.

Based on my limited experience I would tend to not trust anyone especially after reading through some posts of the "Coding Horrors" section tongue.png

@swiftcoder:

Since my project is not going to be a library you might be right. But in my opinion there's nothing wrong with trying to apply some techniques primarily used for APIs to the internal stuff if it makes some things cleaner. I will see how this works out and the good thing is the project is nothing mission-critical so learning new things is what it's all about.

But in my opinion there's nothing wrong with trying to apply some techniques primarily used for APIs to the internal stuff if it makes some things cleaner.

That's kind of my point. PIMPL was never intended to "make things cleaner". It was designed to solve ABI compatibility issues in dynamically linked libraries.

Inside your code base, all it really does is cause a bunch of hassle and boilerplate code.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Pimpl can be implemented without dynamic allocation with C++11:


.hpp:

class Test
{
private:
        struct Impl;
        Impl& m() { return reinterpret_cast<Impl&>(_storage); }
        Impl const& m() const { return reinterpret_cast<Impl const&>(_storage); }
        
        static const std::size_t StorageSize = 1384;
        static const std::size_t StorageAlign = 16;
        
        std::aligned_storage<StorageSize, StorageAlign>::type _storage;
};

.cpp:

Test::Test()
{
    new(&_storage)Impl();
}

Test::~Test()
{
    reinterpret_cast< Impl* >(&_storage)->~Impl();
}

// TODO: also implement copy constructor and assignment operator.

Aether3D Game Engine: https://github.com/bioglaze/aether3d

Blog: http://twiren.kapsi.fi/blog.html

Control

Do you know some techniques beside pimpl to achieve a clean separation between the interface and the implementation? If yes, how do they work and are they field-proven?


Another option I haven't seen mentioned yet is to write your code in a more C-like way - you can put only free functions and forward declarations in the header, then put the class definition and implementation in your CPP file and have the free functions delegate to the class methods. Eg:

// in the header
class Logger;
void Log(Logger& log, const std::string& message);

// in the implementation
class Logger {
public:
    void LogMessage(const std::string& message) {
        std::cout << completeLogMessage << std::endl;
    }
}

void Log(Logger& log, const std::string& message) {
    log.LogMessage(message);
}
This way, there's no dynamic allocation or virtual functions at all. You do sacrifice the syntactic nicety of the constructors, operators, and methods being exposed as part of the class, though. Whether this is a problem or not depends on what you're actually doing with this.

The only place that this matters is when exporting an API for dynamic linking.


And when you care about compile times. You can set up a PIMPL so that changing the implementation doesn't (usually) cause a recompile of code depending on the interface. Long compile times can really hurt your iteration time, so on a large enough project, this sort of thing can be quite helpful. Nobody likes to change a small thing inside a class and then have to wait 20-60 minutes just to test the change.


Pimpl can be implemented without dynamic allocation with C++11:

The problem with this is that you still need to know the size of the Impl class in the header (unless you want to waste a ton of memory and make your classes unnecessarily slower due to poor caching behaviour). So you would have to check/assert for the size at one point and maintain it every time you add members to the Impl, which is not quite the optimal solution.

Aside from that, is there any specific reason you even need C++11 for that?

Can't you just


char[StorageSize] _storage;

And work with it the same way?

This topic is closed to new replies.

Advertisement