You are here: start » charon » tech » encapsulation

Simplified Strict Encapsulation

This method may be used to simplify interfaces to functionalities in c++ libraries. It offers a clear and transparent separation of implementation specifics (which may vary from platform to platform or version to version) from strictly defined and maintained interfaces.


This method relies on the fact that object method calls, when static (not virtual), are generated at compile time, based upon the declared type of an object, not the type of the object itself. This may be illustrated by the example:

  • given the context:
class A {
  public:
    void method0(int a);
    virtual void method1(int a);
};
      
A  myObject;
A* myObjectPtr;
A& myObjectRef;
int a;
  • the following is generated at compile time for:
myObject.method0(a);      ----> A::method0(this, a)                      //1)
myObject.method1(a);      ----> A::method1(this, a)                      //1)
myObjectPtr->method0(a);  ----> A::method0(this, a)                      //1)
myObjectPtr->method1(a);  ----> (myObjectPtr->virtual_table[0])(this, a) //2)
myObjectRef.method0(a);   ----> A::method0(this, a)                      //1)
myObjectRef.method1(a);   ----> (myObjectRef.virtual_table[0])(this, a)  //2)

1) call destination is determined upon compile time, and the actual contents of pointer or reference have no influence.

2) call destination is determined upon run time, and depends on actual contents of pointer or variable represent.

Encapsulation: separating interface from implementation

Two alternatives are regularly used in c++ for achieving encapsulation: private members and virtual methods.

  • Private members:
    • Declaring certain members of the class as private, external access and calling of these members is prevented at compile time. Private members are therefore considered ‘implementation’, and public members are considered ‘interface’.
    • Drawback: The internal mechanism of the implementation is transparent and open to public (as it is included into the .h header file). The result is an interface definition that appears cumbersome, complex, and difficult to read and follow.
  • Virtual methods:
    • Declaring certain methods as virtual allows the inherited classes to override such methods, and the call destination to be established at run time, enabling polymorphism. Encapsulation is usually ensured by defining a base, virtual class, the ‘interface’, and then subclassing it by an ‘implementation’.
    • Drawback: a virtual method call destination is established at run time, which generates overhead.

The new proposition for encapsulation is as follows:

The interface and implementation are defined as separate structures, eg. “tMyClass” (interface) and “tmyclass” (implementation).

An object is then created with a specific new function, and freed with a specific release function:

tMyClass*         new_MyClass(...);   //optional parameters
void              release(tMyClass* object);

In certain cases, the library may not allow objects to be created and/or released freely. Sometimes, other interfaces may be used to maintain such objects, eg:

tResource*        tResourceDB::access(const char* resname);
void              tResourceDB::unlock(tResource* res);

An interface structure is defined as public, base structure with no members, only the interface methods listed, eg:

struct            tResourceDB {
  tResource*      access(const char* resname);
  void            unlock(tResource* res);
  int             number_of_resources();
  const char*     resource_name(int index);
};
      
tResourceDB*      new_ResourceDB(...);   //optional parameters
void              release(tResourceDB* object);

An implementation structure is defined as internal structure or class of any kind (even virtual, if necessary), with its members usually declared public so the rest of the library may access it when objects are interconnected. Eg:

struct            tresourcedb {
  tresource*      resarray;
  int             rescount;
       
  int             find_resource(const char* name);
  bool            load_from_file(const char* fname);
  #ifdef IMPL_2
    void          lock(tresource* res);
    void          unlock(tresource* res);
  #endif
};

When an object is created or otherwise accessed, the implementation object is actually generated, then type cast to interface structure. When it is released or used through the interface, the reverse is done:

tResourceDB*      new_ResourceDB(const char* fname)
  {
    tresourcedb* newdb = new tresourcedb;
    if (newdb) {
      if (!newdb->load_from_file(fname)) {
        delete newdb;
        return nul;
      }
    }
    return (tResourceDB*)newdb;
  }
        
void              release(tResourceDB* db)
  {
    if (db) delete (tresourcedb*)db;
  }

To call an implementation function from within an interface function, or to access its members, a macro can be defined to convert this pointer to proper type.

#define           self ((tresourcedb*)this)
          
static            tresourcedb defaultdb;
static            tResourceDB& defaultDB = *(tResourceDB*)(&defaultdb);
         
tResource*        tResourceDB::access(const char* resname)        
  {
    if (!this) return defaultDB.access(resname);
    int fnd = self->find_resource(resname);
    if (fnd < 0) return nul;
    tresource* res = self->resarray[fnd];
    #ifdef IMPL_2
      self->lock(res);
    #else        
      res->lock();
    #endif
    return (tResource*)res;              
  }
        
void              tResourceDB::unlock(tResource* r)
  {
    if (!this) return defaultDB.unlock(r);
    tresource* res = (tresource*)r;
    #ifdef IMPL_2
      self->unlock(res);
    #else
      res->unlock();
    #endif
  }

Benefits:

  • Minimum overhead due to static linking. Many compilers are now capable of inlining such functions, thus even increasing the performance.
  • Strict separation of interface from implementation. When an interface .h header file is given, only definitions regarding interface may be included, thus making it easier to read. Integrating interface definitions with basic documentation is also made easier.
  • Additional freedom in implementation architecture. Separating it completely from the interface, interconnections between objects on the low level may change from implementation to implementation without any influence on the interface.
  • The ‘interface’ layer (eg. tMyClass functions) provides space for such things as diagnostics, error checking and user-friendliness functions without any influence on the raw, low-level implementation, so that may remain focused on execution and heavy labor code.

Example

void test() {
  tResourceDB* db = new_ResourceDB("database.txt");
  tResource*   res = db->access("test"); //normally unsafe, legal with encapsulation
  tString      txt = res->description(); //normally unsafe, legal with encapsulation
  db->unlock(res);                       //normally unsafe, legal with encapsulation
  release(db);
  //...
}
 
charon/tech/encapsulation.txt · Last modified: 2015/11/18 07:37 by sinister
Recent changes · Show pagesource · Login