- Overview
- Table of Contents
- Special Member Functions: Constructors, Destructors, and the Assignment Operator
- Operator Overloading
- Memory Management
- Templates
- Namespaces
- Time and Date Library
- Streams
- Object-Oriented Programming and Design Principles
- The Standard Template Library (STL) and Generic Programming
- Exception Handling
- Runtime Type Information (RTTI)
- Signal Processing
- Creating Persistent Objects
- Bit Fields
- New Cast Operators
- Environment Variables
- Variadic Functions
- Pointers to Functions
- Function Objects
- Pointers to Members
- Lock Files
- Design Patterns
- Dynamic Linking
- Tips and Techniques
- Using the Swap() Algorithm
- Using class stopwatch for Performance Measurements
- Extending <iostream> to Support User-Defined Types
- Using auto_ptr To Automate Memory Management
- Using auto_ptr To Automate Memory Management, Part II
- Using auto_ptr To Automate Memory Management, Part III
- Using enums as Mnemonic Indexes
- Create Objects on Pre-Allocated Memory Using Placement-new
- Online Books: Placement-new
- Bitwise Operators
- Bitwise Operators II
- Who's this?
- A Reference Guide
- The Virtues of Multiple Inheritance
- Interfaces
- Multiple Inheritance: Construction and Destruction Order
- nothrow new
- POD Initialization
- Object Initialization
- const Declarations
- The Semantics of volatile
- inline Functions
- Project Organization Guidelines
- All About bool
- typedef Declarations
- State of the union
- Dynamic Cast Uses
- Integrating C and C++
- const-Correctness
- const-Correctness: Advanced Issues
- Sprucing Up Legacy Code
- Virtual Constructors
- Naming Names
- Function Calls
- Speaking Standardese (updated)
- Speaking Standardese: the One Definition Rule
- Declarations and Definitions
- More on Declarations and Definitions
- The Most Vexing Parse
- Finally, At Last
- Sound Bytes (Admittedly Off Topic)
- Local Classes
- Complex Arithmetic
- Floating Point Woes
- String Manipulation
- The Object Model
- The Object Model II
- The Object Model III
- Temporary Objects
- Temporary Objects: Advanced Techniques
- Over-Engineering
- Security Enhancements
- Drop the (automatic) Pilot
- Choosing the Right Container
- Choosing the Right Container II
- Choosing the Right Container, Part III
- Arrays and Pointers
- Low-Level File I/O
- Low-Level File I/O Part II
- static Declarations, Part I
- static Declarations, Part II
staticInitialization Order- Revisiting the Deprecation of File-Scope Static
- Virtual Memory and Memory Mapping
- Cellular Phone Programming Guidelines
- The Handle/Body Idiom
- Whole Program Optimization, Part I
- Whole Program Optimization, Part II
- Manipulating Directories
- Window Dressing
friendDeclarationsfriendPart II: the Interaction of Friendship and Template Classes- Forcing Object Allocation on Specific Storage Types
- Lazy Evaluation
- Cache and Carry
- Controlling a Container’s Capacity
- Non-Blocking I/O, Part I
- Non-Blocking I/O, Part II
- Using Unions for Automatic Conversion
- Launching a Child Process
- switch Statements
- Introducing the "struct Hack"
- Scoped Enumerators
- Doing Statistics with STL
- Fixing the "Unresolved External" Linkage Error
- Understanding Calling Conventions
- Understanding the Empty Base Optimization
- Implementing RPC with the door Library, Part 1
- Implementing RPC with the door Library, Part 2
- Eliminating Two Common Pointer and sizeof Bugs
- Command Line Arguments
- Performance Myths Busting
- Tag Names And Types Part I
- Tag Names And Types Part II
- The Infamous goto
- Trimming Strings
- Can Objects Live Forever? Part I
- Can Objects Live Forever? Part II
- Five Ways to Improve Your Functions
- Member Aggregate Initialization
- Five Futile Coding-Style Debates
- The Good Parasite Idiom: An Exercise in OOD
- The Good Parasite Idiom: An Exercise in OOD, Part II
- The Good Parasite Idiom: An Exercise in OOD, Part III
- Ten Techniques to Reduce the Size of Your Classes, Part I
- Ten Techniques to Reduce the Size of Your Classes, Part II: Inheritance Issues
- Ten Techniques to Reduce the Size of Your Classes, Part III
- Ten Techniques to Reduce the Size of Your Classes IV
- Taking the Address of an Object with an Overloaded Operator &
- strcpy() -- How and Why Does It "Just Work"?
- Anonymous Structs
- Five Easy Ways to Reduce The Size of your Executables
- Standard Layout Classes and Trivially Copyable Types, Part I
- Standard Layout Classes and Trivially Copyable Types, Part II
- Five Simple Code Sanity Checks
- Five Things You Need to Know About C++11 Unions
- A Tour of C99
- A Tour of C1X
- C++0X: The New Face of Standard C++
- C++0x Concurrency
- The Reflecting Circle
- We Have Mail
- The Soapbox
- Numeric Types and Arithmetic
- Careers
- Locales and Internationalization
Interfaces
Last updated Jan 1, 2004.
Multiple inheritance in C++ has two flavors, namely interface inheritance and implementation inheritance. Today, I will explain the notion of interfaces, and present the syntactic cues that distinguish them from concrete classes.
Interfaces Explained
A class's interface consists of its public members. Usually, these would be member functions, although in certain rare cases, local data types and even data members may also be included. By contrast, the non-public members of a class are said to be its implementation. For example:
class Date
{
public: //Date's interface appears in bold
Date();
int getCurrentDate(string & datestr) const;
Date operator +(const Date & d) const;
private: //Date's implementation
int day, month, year;
bool daylight_saving;
};
Although Date has an interface, the class itself is not an interface because it contains non-public members. By contrast, the following class is an interface:
class File
{
public:
explicit File(const string& name, int mode=0x666);
virtual int Open();
virtual int Close();
virtual ~File();
};
Notice that every member function in File is both public and virtual (except for the constructor, of course). These syntactic cues suggest that File should serve only as a base for other, more specialized classes. Certain frameworks mark this attribute explicitly, by using the keyword struct instead of class. As you already know, the only difference between the two in C++ is that struct has public access by default, whereas class entails private access. Therefore, the previous example could be rewritten like this:
struct File //explicitly indicates that this is an interface
{
explicit File(const string& name, int mode=0x666);
virtual int Open();
virtual int Close();
virtual ~File();
};
Another characteristic of interfaces is that they merely declare member functions. The classes derived from them should define, or implement these functions. The use of virtual member functions isn't sufficient for this purpose, because a derived class isn't forced to override any of the base class's member functions. Furthermore, the author of class File must provide a default implementation for its members anyway, or else the linker will complain. Fortunately, there's a better way to achieve this goal.
Abstract Classes
Unlike an ordinary virtual function, a pure virtual function doesn't have to be defined. It can be declared in the class body without an implementation. C++ uses a special syntactic form to mark pure virtual functions by adding the pure specifier =0; to a declaration of such a function. For example:
struct File
{
explicit File(const string& name, int mode=0x666);
virtual int Open()=0; //pure virtual
virtual int Close()=0; //ditto
virtual ~File()=0; //a dtor may be pure virtual too
};
A class that has at least one pure virtual function is called an abstract class. An abstract class is more or less synonymous with our notion of an interface.
At last, we've turned File into a proper interface. Notice that the use of the pure specifier ensures two important things. First, it guarantees that users cannot instantiate an object of an abstract class:
File file("data.txt"); //compilation error: File is an abstract class
You may, however, define pointers and references to an abstract class to achieve polymorphic behavior:
File *pfile; //OK, pfile should point to a derived object void func(File &ref);
C++ ensures that classes derived from an abstract class also obey this rule. You may instantiate objects of a class derived from an abstract class only if it implements all the pure virtual functions it inherited from the base class(es).
Combining Multiple Interfaces
You may combine several interfaces in a single concrete class. For example, suppose you want to define a class that represents a file stored on removable media. You can combine two interfaces like this:
//an interface the represents an abstract removable device
struct RemovableMedia
{
virtual int Explore()=0; //view contents
virtual int Save(const string& file)=0;
virtual size_t Retrieve(const string& file, void* buf)=0;
};
//public inheritance implied from 'struct':
class RemovableFile: RemovableMedia, File
{
//...implement all the pure virtual member functions
};
You may combine more interfaces to create a more specialized object:
class RemovableFile: RemovableMedia,
File,
Encrypted
{
//...implement all the pure virtual member functions
};
Remember that the interfaces are all publicly inherited because of the struct keyword. However, to be more explicit about this (and to be on the safe side, should the interfaces' author decide to use class instead of struct), you may use the public keyword:
class RemovableFile: public RemovableMedia,
public File,
public Encrypted
{
//...implementation
};
The DDD Strikes Again
The fact that interfaces don't have data members still doesn't prevent ambiguities. If, for example, one of the interfaces is inherited multiple times throughout the derivation lattice, you're in trouble:
class TextFile: public File, public Encrypted
{};
class NetworkFile: public File, public Encrypted
{};
class DataFile: public TextFile, public NetworkFile //DDD!
{};
File *pf = new DataFile;//error; ambiguous base class
DataFile happens to have two copies of File and Encrypted. To avoid this, use virtual inheritance:
class TextFile: public virtual File,
public virtual Encrypted
{};
class NetworkFile: public virtual File,
public virtual Encrypted
{};
class DataFile: public virtual TextFile,
public virtual NetworkFile //now fine
{};
Pure Virtual Functions: Final Refinements
Generally speaking, a pure virtual function needn't be implemented in an abstract class, nor should it be. However, in one special case, namely a pure virtual destructor, it must have an implementation. The reason is that a destructor of a derived object automatically invokes its base class' destructor, recursively. Eventually, the abstract class's destructor is called too. If it has no implementation, you'll get a runtime crash. Therefore, it's important to provide a definition for every pure virtual destructor:
struct File
{
explicit File(const string& name, int mode=0x666);
virtual int Open()=0;
virtual int Close()=0;
virtual ~File()=0; //must have a definition
};
File::~File() {} //dummy implementation
In all other cases, defining a pure virtual function is optional (and rather unusual!).
Summary
The use of pure virtual functions enables you to specify only an interface, without an implementation. As a rule of thumb, an interface class should be compact, containing no more than five pure virtual functions. This way, you may define multiple specialized interfaces that a derived class can combine as needed. Lumping dozens of pure virtual functions in a single fat interface is a design mistake because derived classes will be forced to implement all of these functions, even if they intend to use only a small portion of them.



