Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 16.  Keywords


16.1. interface

The word interface has found common use in the Common Object Request Broker Architecture (CORBA) and the Component Object Model (COM), taking the meaning of a fully abstract class, that is, one in which every member is pure virtual. It is such a great thing to denote—from the perspective of a reader of code at least—that it has found a fully fledged role as a keyword in several newer languages, including D, Java, and C#/.NET. In the COM headers, interface has been defined using #define; Microsoft exhibited their customary disregard for the preprocessor namespace, albeit one is inclined to forgive them in this case.

At first glance, with COM spectacles on, the absence of this keyword seems like an imperfection that could easily be amended by adding it to the language. But there are three objections. First, it would serve no use with current COM headers, since it would be redefined to struct as it is now. Second, it might break existing COM-independent code, which may be "legitimately" using the word as a variable name. Finally, and most compellingly, there is no reasonable definition.

As we see in section 19.8, there are good reasons for flouting good C++ idioms when implementing reference-counting interfaces. COM interfaces (and those of other reference-counted infrastructures) are concerned only with the methods QueryInterface(), AddRef(), and Release() or their equivalents along with the additional interface-specific methods.



interface ICompress


  public : IUnknown


{


  // IUnknown methods


  virtual HRESULT QueryInterface(REFIID riid,  void **ppvObject) = 0;


  virtual uint32_t AddRef() = 0;


  virtual uint32_t Release() = 0;





  // ICompress methods


  virtual HRESULT Compress( byte_t    *pyin


                          , uint32_t  cbIn


                          , byte_t    *ppyOut


                          , uint32_t  *pcbOut


                          , int32_t   *pdwFlags) = 0;


};



They specifically do not contain constructors or destructors, since the rules of the reference-counting architecture prohibit an interface client of an object from deleting it, and a virtual destructor would interfere with binary layout and thus language interoperability. Even if destructors were not proscribed by the rules of the architecture, the fact that COM is language neutral (to the degree that an implementing language is able to formulate the appropriate calling conventions and parameter types), any provision of a C++ destructor would be meaningless to other languages.

Note that it is perfectly reasonable, and fairly common, to have a reference-counting architecture with similar characteristics (i.e., no destructors) to COM, which has nothing to do with COM. Indeed, I wrote one that worked on various UNIX flavors and VMS (see Chapter 8).

In other interface-based scenarios that are purely C++ and do not use reference counting, it is common to see that an interface does contain a pure-virtual destructor to ensure that there will not be incomplete destruction [Meye1998]; sometimes this is the only member.



interface IRoot


{


public:


  virtual ~Root() = 0;


};





inline IRoot::IRoot() // Implementation required for pure dtor


{}



Both of these flavors of "interface" imply lifetime control (see Chapter 5): one uses reference counting and the other uses explicit deletion. However, when we're not causing our client code to be concerned with the lifetime of the interface (or, rather, the implementing instance) then the model can be very simple and concerned entirely with the functionality provided. This is the model of interface used in D, Java, and .NET:



interface IIdentity


{


  virtual int GetId() = 0;


}



So should interface be used solely for reference-counting interfaces, for destructible interfaces, or reserved for "regular" interface classes? Should its meaning be implementation dependent? It's not an answerable question, since C++ (thankfully) supports all these techniques.

Perhaps we could compromise with a weakened definition that mandates that all interface methods be pure virtual? But this would also fail, since (as we see in section 8.2) it is actually useful to insert nonvirtual methods into an interface definition, at least when it is visible in C++ compilation units.

The only worthwhile definition of interface I can think of is that it has the following characteristics:

  • Its default access is public, as is the case with struct.

  • It may contain nonvirtual functions (and their implementations).

  • It may contain pure virtual functions.

  • It may not contain any nonpure virtual functions (and their implementations). The only definition for a virtual function allowed is that for any pure virtual destructors, since they are required by the C++ object-model. However, such pure virtual destructor implementations must be strictly empty (as shown in the earlier example).

  • It may not inherit from any class-key type (i.e., struct/class/union; C++-98: 9.1) except another interface.

Of course, all the behaviors facilitated by this definition are already expressible with the existing language, so it's hardly likely that there will be serious interest in adding this keyword. Nonetheless, I think it's worthwhile to have highlighted the issues, since they are often used but little discussed. In section 8.2.7 we looked at a related keyword that would be much more useful to have, pinterface.

16.1.1 pinterface

Although I may think that there's probably no good way of defining an interface keyword, I would suggest there's a pretty strong case for some other keyword, for example, pinterface, by which all the effective but incredibly verbose gunk of the portable vtables technique (see Chapter 8) could be encapsulated. In other words, a pinterface would be a struct that could contain only pure virtual methods or nonvirtual methods, and no member data, and would have a common prescribed layout (a contiguous packing of architecture-sized pointers) for itself and its vtable for a given architecture. Then we could have portable polymorphism with ease.


      Previous section   Next section