C++ – Best way to design a class interface passed to library/plugin

cinterfaceslibrariesplugins

I have an application comprising a bootstrapper executable, a core library and several plugins (shared libraries). The core library is implicitely linked to all components, the plugins get linked/loaded explicitely by the core library. When the plugins functions get called, I pass several objects defined in the core library. Now I ask myself which is the best way to design the interface of these classes.

There are three ways I can think of.

  1. Design a simple class and pass it. This is the current approach, but the interface is messy because the core needs additional functions on this object, currently realized with friend relationships and private access modifier so that the clients/plugnis cant access it. I think this is the worst approach since the client interface is really ugly.
  2. Create an ABC (interface) and add the core functionality in the concrete subclass and pass the object as the ABC to the plugins. This looks fine, but somehow I am not sure if this is good design since on the one hand interfaces are not really a C++ concept and theres only one class that inherts the ABC (seems like a smell to me) and on the other hand the compiler complains about weak vtables emitted in all TUs. (I tried but I dont get this)
  3. Create a mediator/facade class and hide the interface needed by the core library. The con in this approach beeing the extra indirection.

Would be nice if somebody could complement this list and elaborate the pros and cons of the approaches. Finally if it is possible I'd appreciate a recommendation. Thank you.

Best Answer

Of those three, number 2 is the way to go: introduce an interface. Classes with pure virtual functions are a legitimate C++ concept. The language supports interfaces even though it has no special syntax for them.

The problem with your first suggestion (design a simple class) is that any change to the declaration of the class breaks binary compatibility, so all your plugins would have to be recompiled with the new version. In particular, adding, removing, or reordering members could break binary compatibility, even for private members. There are very few changes you can safely make to a .h file. It is therefore important to keep any unnecessary information out of your headers.

One such technique is to use polymorphism: define an interface that exposes a set of virtual methods, and an implementation that inherits from the interface. Since clients will only depend on the interface, any changes to the implementation are safe. This is basically the solution number 2 you've sketched out. The drawback is that polymorphic objects need reference semantics: you must pass them by reference or by pointer, and such types are not copy-constructible or copy-assignable.

A similar solution is to use incomplete types (pImpl idiom). In a header file, a class is declared with various methods. It only has a single member, which is a pointer to an incomplete type (usually with std::unique_ptr<…>). In a .cpp file, the incomplete type is declared and defined. This declaration is not accessible outside of the cpp file. This private type contains all implementation details, especially all member fields. Also within this file, we delegate all public methods to the private implementation. Since the private type is known, such types can also be made copy-constructible and copy-assignable.

In both of these cases, we've inverted our dependencies so that client code will not depend on implementation details. As the interface will usually be very stable, the client code will only need to be recompiled when the interface actually changes.

                 +-----------+
           .---> | Interface | <---.
depends on |     +-----------+     | depends on
           |                       |
   +----------------+         +--------+
   | Implementation |         | Client |
   +----------------+         +--------+

For polymorphism, the interface is an abstract base class with pure virtual methods, the implementation another class. For pImpl, the interface is a .h file with an incomplete type, and the implementation is a compilation unit containing the definition of the incomplete type.

In general, using the pImpl idiom is easier and more flexible than using inheritance.

But with regards to your goal of making more parts of the implementation visible to your internal code than to the clients, a clear interface will probably be less confusing.

Related Topic