Designing an efficient interface that decouples a component from its client is a fairly routine task that's easy to mess up. A component is supposed to be highly reusable, flexible and easy to use, not to mention well isolated. Satisfying these diverse (and often mutually exclusive!) requirements is no easy feat and often comes down to certain tradeoffs in the component interface.
There's no one right answer to the big question of how exactly a component's classes should be exposed to the client in terms of isolation, but the problem boils down to three main issues: exposing the interface, encapsulating the implementation details and providing a way to instantiate the component's classes. We'll have a look at some of the popular choices, just as you'd probably do when considering various alternatives for a real-life production quality C++ component interface.
We'll use a classical Car & Garage example for the code snippets below.
// Component header, take #1
bool Drive(int speed);
AddCar(const Car& car);
This is a working interface that will probably do its job, and in many cases it is in fact the model of choice. For one thing, it's the fastest method of all. No complications, no problems. Instantiate the classes as you're all set. However, several potential issues are worth noting if this code is supposed to be a component interface:
Unchecked instantiation. A client can spawn as many instances of the component's classes as they please (both on the stack and on the heap). This becomes a nasty issue if those classes grow in size (storing large objects on the stack can be sub-optimal) or come to require certain instantiation restrictions (typically such as only one Garage object per client application). Since you never really know what the future requirements to the component might be, taking precautions against these problems early on seems reasonable enough.
Albeit somewhat flawed, I find this isolation model useful for libraries that contain a loose affiliation of unrelated and relatively simple helper classes that just do not warrant a more sophisticated approach.
Let's try and refine the above interface to deal with its inherent problems.
// Component header, take #2
static Garage& GetInstance();
Garage& operator=(const Garage&);
This time we applied standard solutions to control instantiation of the component classes and insulate their private contents: the singleton pattern and the pImpl idiom respectively. Note how the client doesn't have the definition of the pImpl classes and therefore cannot mess with them. Neither can the client create more than one instance of the Garage class (you can also impose other instantiation restrictions similarly). Job done? Umm... Not quite.
It appears that by addressing the problems with the initial interface we've inadvertently introduced other serious issues:
Object's lifetime control is far from perfect. Singletons and similar instantiation restrictions imply that either a) the class obscurely controls the lifetime of its own object (e.g. Meyers singleton) or b) the client has to manually destroy the object (e.g. Gamma singleton). Any special function that produces a class object is haunted by this ugly ambiguity - unless it's returning a smart pointer that is. Sigletons are also subject to static initialization and finalization issues that are somewhat beyond the scope here, but let's just say I don't recommend using them without a very good reason.
Performance overhead. PImpl has to be allocated dynamically, thus introducing a minor performance penalty for every class instantiation.
Code editing is burdensome. Insulation has its price: adding even a single function to the pImpl'ed class means you'll have to edit the back-end class as well as the interface class. If both classes are non-inline (which is probably the way they are supposed to be), it means editing too much code for comfort.
Singletons surely have their valid uses, but they almost never make it to interfaces of my components except as occasional supplements to make pre-fabricated shared versions of some of the other classes. As for the pImpl idiom, I find it to be only marginally useful compared to other alternatives.
Looks like the interface may need yet another makeover. Oh well.
// Component header, take #3
virtual bool Drive(int speed) = 0;
virtual bool Stop() = 0;
typedef boost::shared_ptr<ICar> PCar;
virtual AddCar(PCar car) = 0;
typedef boost::shared_ptr<IGarage> PGarage;
The classes have gone abstract, and there's a simple factory in place to produce objects of those classes. Note how the factory is returning smart pointers this time, the magnificent boost::shared_ptr to be precise (you can also use std::shared_ptr if your compiler supports C++11, which it should). For some purposes std::auto_ptr would also do a satisfactory job in this case. So what do we have here?
Perfect encapsulation and insulation. Absolutely no class internals are revealed, in fact even the implementation classes themselves are hidden.
Controlled instantiation. The factory functions are in complete control over how the classes are instantiated, how many instances can be created etc.
Convenient lifetime control. The client can use the objects in the "fire and forget" mode most of the time while easily delaying or precipitating destruction of the objects as they please through controlling the lifetime of the original smart pointers and copies thereof. shared_ptr supports custom deleters that don't affect the type of shared_ptr template instantiation (see here, for instance), so this solution also supports allocating memory for the objects with something other than operator new.
Minor editing inconvenience. Adding new functions to the classes entails changes to both back-end implementations and front-end interfaces, but we're talking pretty small changes here. Definitely worse than the naive approach #1 but much better than the pImpl case.
Minor performance hit. Creating objects on the heap and using virtual functions makes for a barely noticeable but positively present performance hit. It's worse than both #1 and #2 in this respect because as many as two dynamic memory allocations will be done per class instantiation: one for the object itself and another for the reference counter internal to shared_ptr.
Unfortunate susceptibility to the infamous new/delete issue with statically linked CRT in a DLL library. For this reason, I prefer not to author DLL class libraries if at all possible.
Where does this leave us? In my humble opinion, the interface & shared_ptr solution by far outclasses other conceivable alternatives and makes for an almost perfect all-round instantiation model for a component interface in C++. I've used it too many times to count, and never had any significant problems or encountered major limitations except for the abovementioned CRT issue.
I will continue looking at other, more complicated aspects of component interface design in future articles, but this is it for today.