Page #28 (Virtual Methods in C and Memory Layout)

< BACK  NEXT >
[oR]

Interface as an Abstract Base Class

If you recall, a major problem in achieving compiler independence was nonstandard name-mangling schemes used by various compilers. However, as was seen in the earlier section, if a client calls a virtual method of a class, the generated machine language code for the client side does not refer to the method by its symbolic name. Instead, it uses the vtbl mechanism to invoke the method. Thus, if a client calls only the virtual methods, name mangling is no longer an issue.

Once we opt for using the virtual methods mechanism, three new issues arise.

If the interface class contains data members, the placement of vptr in the memory layout is chosen by the vendor. One vendor may wish to place it before the data members and the other after the data members. This problem can easily be avoided by not declaring any data members in the interface definition. This fits very well with our notion of hiding implementation details from the clients.

The second problem is related to multiple inheritance of an interface class. In this case, the memory layout of the class will contain more than one vptr, as we have seen earlier. However, there is no agreed-upon standard in the ordering of vptr s. We can overcome this problem by limiting the derivation to just one base class.

The third problem is related to the ordering of vtbl entries. The clientside compiler has to assume that the first entry in the vtbl is the address of the first virtual function declared in the interface class, the second entry is the address of the second virtual function declared, and so on. However, with overloaded virtual functions, this assumption is not valid across all compilers, as we witnessed earlier. Therefore, overloaded function declarations should not be allowed in the interface class.

Furthermore, by defining the virtual functions as pure, we can indicate that the interface class defines only the potential to call the methods, not their actual implementation.

In order to ensure that all compilers generate equivalent machine language code for client-side method invocation for an interface, the interface class definition should:

  • contain only pure virtual methods

  • not contain any overloaded virtual methods

  • not contain any members variables

  • derive from at most one base class where the base class also adheres to the same restrictions

By following these rules for defining interface classes, we can achieve our goal of compiler independence for interface class definition.

An interface class defined this way is called an abstract base class. The corresponding C++ implementation class must derive from the interface class and override each of the pure virtual methods with meaningful implementation. As we have seen earlier, the memory layout of such an implementation class is just a binary superset of the interface class.

graphics/01icon01.gif

The flexibility lost by restricting the interface definition to the abstract base class has not been much of a problem. Also, calling a virtual function results in just one more machine language instruction and is negligible compared to the number of instructions a typical function executes. However, the benefits obtained by defining an interface as abstract based class is well worth it.


The following code shows the definition of the IVideo class redefined as an abstract base class and the corresponding implementation of the CVcr class.

 // Video.h - Definition of interface IVideo  class IVideo  { public:    virtual long _stdcall GetSignalValue() = 0;  };  // File vcr.h  #include "Video.h"  class CVcr : public IVideo  { public:    CVcr(void);    long _stdcall GetSignalValue();  private:    long m_lCurValue;    int m_nCurCount;  }; 

Not only does this mechanism solve the compiler dependency problem, it also solves some other weaknesses with our old technique of using the opaque pointers. If you recall, the old technique had two weaknesses:

  • Performance penalty: Each method call incurs the cost of two function calls one call to the interface, and one nested call to the implementation.

  • Error prone: For a large class library with hundreds of methods, writing the forward call is not only tedious, but also susceptible to human errors.

Deriving the implementation class from the abstract base class addresses both of these weaknesses.

We now have a new problem, however. The C++ compiler will not let you instantiate an abstract base class. Only concrete classes, such as the implementation class, can be instantiated. However, revealing the implementation class definition to the client would bypass the binary encapsulation of the interface class.

A reasonable technique for clients to instantiate a base class object is to export a global function from the DLL that will return a new instance of the base class. As long as this function is declared as extern C , the client-side code will not run into any name-mangling problem.

 // Video.h  extern "C" IVideo* _stdcall CreateVcr();  // Vcr.cpp (implementation)  IVideo* _stdcall CreateVcr(void)  {   return new CVcr;  } 

With this factory mechanism in place to create the needed instance, our TV client code will look like the following:

 #include "Video.h"  #include <iostream.h>  int main(int argc, char* argv[])  {   int i;    IVideo* pVideo = CreateVcr();    for(i=0; i<10; i++) {         long val = pVideo->GetSignalValue();          cout << "Round: " << i << " - Value: " << val << endl;    }    delete pVideo;       // we are done with it    return 0;  } 

This code will almost work, except for one subtle flaw note that new for pVideo occurs in the VCR executable, and delete occurs in the TV executable. As a result, the destructor for class CVcr is never invoked, and if the client uses a different C++ compiler than the server, the result of memory deallocation in the client is unpredictable.

An obvious solution is to make the destructor virtual in the interface class. Unfortunately, the position of the virtual destructor in the vtbl can vary from compiler to compiler.

A workable solution is to add an explicit Delete method to the interface class as another pure virtual function and let the derived class delete itself in the implementation of this method. The code snippet shown here is the revised definition of interface IVideo class and its implementation class, CVcr.

 // Video.h - Definition of interface IVideo  class IVideo  { public:    virtual long _stdcall GetSignalValue() = 0;    virtual void _stdcall Delete() = 0;  };  // File vcr.h  #include "Video.h"  class CVcr : public IVideo  { public:    CVcr(void);    long _stdcall GetSignalValue();    void _stdcall Delete();  private:    long m_lCurValue;    int m_nCurCount;  };  // File vcr.cpp  void CVcr::Delete()  {   delete this;  } 

With this mechanism in place, the revised client code looks like the following:

 int main(int argc, char* argv[])  {   int i;    IVideo* pVideo = CreateVcr();    for(i=0; i<10; i++) {     long val = pVideo->GetSignalValue();      cout << "Round: " << i << " - Value: " << val <<  endl;    }    pVideo->Delete();    return 0;  } 

In summary, by using an abstract base class for defining the interface, adding Delete functionality as part of the interface specification, and using a factory method to obtain the interface, we have finally achieved compiler independence.

Let s now consider how to achieve vendor independence, or, in other words, can we select a VCR from the vendor of our choice?


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net