Creating an Interface with an Abstract Base Class
Problem
You need to define an interface that subclasses will implement, but the concept that the interface defines is just an abstraction, and is not something that should be instantiated itself.
Solution
Create an abstract class that defines the interface by declaring at least one of its functions as pure virtual. Subclass this abstract class by clients who will use different implementations to fulfill the same interface guarantees. Example 8-10 shows how you might define an abstract class for reading a configuration file.
Example 8-10. Using an abstract base class
#include #include #include using namespace std; class AbstractConfigFile { public: virtual ~AbstractConfigFile( ) {} virtual void getKey(const string& header, const string& key, string& val) const = 0; virtual void exists(const string& header, const string& key, string& val) const = 0; }; class TXTConfigFile : public AbstractConfigFile { public: TXTConfigFile( ) : in_(NULL) {} TXTConfigFile(istream& in) : in_(&in) {} virtual ~TXTConfigFile( ) {} virtual void getKey(const string& header, const string& key, string& val) const {} virtual void exists(const string& header, const string& key, string& val) const {} protected: istream* in_; }; class MyAppClass { public: MyAppClass( ) : config_(NULL) {} ~MyAppClass( ) {} void setConfigObj(const AbstractConfigFile* p) {config_ = p;} void myMethod( ); private: const AbstractConfigFile* config_; }; void MyAppClass::myMethod( ) { string val; config_->getKey("Foo", "Bar", val); // ... } int main( ) { ifstream in("foo.txt"); TXTConfigFile cfg(in); MyAppClass m; m.setConfigObj(&cfg); m.myMethod( ); }
Discussion
An abstract base class (often referred to as an ABC) is a class that can't be instantiated and, therefore, serves only as an interface. A class is abstract if it declares at least one pure virtual function or inherits one without implementing it. Thus, if a subclass of an ABC needs to be instantiated, it has to implement each of the virtual functions, which means that it supports the interface declared by the ABC.
A subclass that inherits an ABC (and implements all of its pure virtuals) upholds the contract defined by the interface. Consider the classes MyAppClass and TXTConfigFile in Example 8-10. MyAppClass has a pointer member that points to an object of type AbstractConfigFile:
const AbstractConfigFile* config_;
(I made it const because MyAppClass should not be changing the config file, only reading from it.) Users can set the config file for MyAppClass with a setter member function, setConfigObj.
When it is time to use the config file for MyAppClass, as MyAppClass::myMethod does, it can call any of the functions declared on AbstractConfigFile without regard for the actual kind of config file that was used. It could be a TXTConfigFile, XMLConfigFile, or anything else that inherits from AbstractConfigFile.
This polymorphic behavior is the benefit of inheritance in general: if your code refers to a base class object, invoking virtual functions on it will dynamically use the correct versions of subclasses of that class, so long as the actual object you're referring to is an object of that subclass. But this is the case whether the base class is an ABC or not, so what's the difference?
There are two differences. A pure interface class (an ABC that provides no implementation) serves only as a contract that subclasses must obey if they want to be instantiated. Often, this means that the is-a test for a subclass of a pure interface class may fail (meaning you can't say that an object of the subclass is an object of the base class), but that the behaves-like-a test succeeds. This permits you to have some separation of what something is versus what it can do. Think of Superman. He is a person, but he is also a superhero. Superheroes can fly like a bird, but it is not correct to say that a superhero is a bird. You might design a class hierarchy for Superman like I did in Example 8-11.
Example 8-11. Using a pure interface
class Person { public: virtual void eat( ) = 0; virtual void sleep( ) = 0; virtual void walk( ) = 0; virtual void jump( ) = 0; }; class IAirborne { public: virtual void fly( ) = 0; virtual void up( ) = 0; virtual void down( ) = 0; }; class Superhero : public Person, // A superhero *is* a person public IAirborne { // and flies public: virtual void eat( ); virtual void sleep( ); virtual void walk( ); virtual void jump( ); virtual void fly( ); virtual void up( ); virtual void down( ); virtual ~Superhero( ); }; void Superhero::fly( ) { // ... } // Implement all of the pure virtuals in Superhero's superclasses... int main( ) { Superhero superman; superman.walk( ); // Superman can walk like a person superman.fly( ); // or fly like a bird }
Lots of different kinds of things fly, though, so you don't want an interface called, for example, IBird. IAirborne indicates that anything that supports this interface can fly. All it does is allow client code to rest assured that if it is working with an object derived from IAirborne, the client code can call fly, up, and down.
The second difference is that an ABC can define an abstract entity that makes no sense as an object because it is inherently general. In this case, the is-a test holds for the inheritance, but the ABC is abstract because, by itself, it has no implementation that can be instantiated as an object. Consider the AbstractConfigFile class in Example 8-10: Does it make any sense to instantiate an AbstractConfigFile? No, it only makes sense to instantiate different kinds of config files that have concrete representation.
Here is a quick list of rules regarding abstract classes and pure virtual functions. A class is abstract if:
- It declares at least one pure virtual function
- It inherits, but does not implement, at least one pure virtual function
An abstract class cannot be instantiated. However, with an abstract class you can:
- Have data members
- Have nonvirtual member functions
- Provide implementations for pure virtual functions
- Do most of the things you can in an ordinary class
In other words, you can do just about everything you can do with an ordinary class except instantiate it.
Using ABCs in C++ requires discretion when it comes to implementation. Whether you use an ABC as a pure interface or not is up to you. For example, assume for a moment that in the superhero example I decided that the Person class should be abstract, but since every kind of person has a first and last name, I want to add those members to the class and the associated getter and setter member functions so that authors of subclasses don't have to.
class Person { public: virtual void eat( ) = 0; virtual void sleep( ) = 0; virtual void walk( ) = 0; virtual void jump( ) = 0; virtual void setFirstName(const string& s) {firstName_ = s;} virtual void setLastName(const string& s) {lastName_ = s;} virtual string getFirstName( ) {return(firstName_);} virtual string getLastName( ) {return(lastName_);} protected: string firstName_; string lastName_; };
Now, if the Superhero subclass wants to override one of these functions, it can. All it has to do is use the base class name to qualify which version it is invoking. For example:
string Superhero::getLastName( ) { return(Person::getLastName( ) + " (Superhero)"); }
Incidentally, you can still make these functions pure and provide a default implementation. You just have to use the = 0 syntax following the declaration and put the actual definition somewhere else, like this:
class Person { // ... virtual void setFirstName(const string& s) = 0; // ... Person::setFirstName(const string& s) { firstName_ = s; }
By doing this, you force subclasses to override it, but they can still call the default version if they want to by using the fully qualified class name.
Finally, if you provide a virtual destructor in your base class (pure or not), you have to provide a body for it. This is because the subclass destructor will call the base class destructor automatically.