Lit Window Productions

Documentation
Download
simpler UI Coding
Articles
How it works
Rapid UI development
Data Adapters 1

RSS Feed
Whats this?

Contact
Privacy

Fast, easy dialog editing and code generation with Anthemion’s Dialog Editor for wxWidgets...

Data Abstraction Layer – portable C++ reflections library

Abstract

This article contains a technical description of the Lit Window Library’s “data abstraction layer”. The “data abstraction layer” is a platform and compiler independent “reflections” mechanism. It offers a way for generic algorithms to access members and variables of type definitions that are not known when the algorithm is written and compiled.

Part 1 – a generic interface for unknown types

The problem – reinventing the wheel…

Component reuse in source or binary form saves a lot of time and lies at the heart of good coding practices. C++ has many language elements that promote reuse, but there is an entire category of problems where C++ lacks the means to create reusable components.

All coding tasks that require access to member variables of an aggregate or access to the elements of a container must have the data type of the member defined at compile time. When you code functions to stream objects to storage, display member variables in a user interface or bind structs to a database tables, the type of the member variables has to be known at compile time. It is not possible to write reusable code for a class whose definition is yet unknown.

As a result, programmers rewrite essentially the same functions over and over again for every individual class:

    void WriteSettings(ostream &o, FooBar aFooBar)
    {
      o << aFooBar.memberOne
        << aFooBar.memberTwo
        << aFooBar.memberThree
    … and so on
    }

Every member variable has to be listed individually and in multiple functions: WriteSettings, ReadSettings, FillListBox, SaveToXml, ReadFromXml, Copy

The solution – data abstraction layer

The solution presented here is called “data abstraction layer”. The name “data abstraction layer” was inspired by the “hardware abstraction layer” modern operating systems use. The mechanism bears resemblance to java reflections, but was designed with a different emphasis. Most reflections or runtime type information schemes are concerned with language details. They allow a programmer to query the const-ness, whether the object is a pointer, the exact inheritance hierarchy and so forth.

The data abstraction layer allows this, but its primary purpose is to allow access to members of an unknown class. It does so by providing a layer of abstraction using interface objects called “data adapters”. What distinguishes this approach further from other, similar work is that the data abstraction layer also provides data adapters for containers.

Generic algorithms use data adapters to access the members of an unknown class. Iterators allow iterating over all members of the entire class hierarchy. Data adapters come in three different varieties: accessor, aggregate and container. All data adapters work like pointers. They point to the concrete object, hiding the specific object layout and allow access to the object via a generic interface. They ‘adapt’ the concrete object interface to a common, generic interface.

Accessor adapters point to opaque objects. These objects are fundamental in the sense that the accessor  adapter cannot provide any information about the internal structure of the object.

Aggregate adapters point to aggregate objects, i.e. class or struct definitions. These objects are aggregates of other objects. The aggregate adapter provides a means to iterate over all members of the aggregate, providing access to the internal structure of the object.

Container adapters point to container objects. Containers are collections of objects of similar type. The container adapter provides means to iterate over all elements in a collection.

For brevity the ‘adapter’ part of the name is omitted from now on. Accessor adapters are called accessors, aggregate adapters are called aggregates and container adapters are called containers. To distinguish the adapter from the actual object it points to, objects are referred to as concrete objects. Thus a container adapter points to a concrete container object, or in short: a container points to a concrete (container) object.

Fundamental types – the accessor class

The most basic adapter is the accessor. Accessors point to a concrete object and expose its properties through a generic interface. The interface allows setting/querying the value using a string or int representation, testing and comparing various properties and querying information about the class/type name of the concrete object or its member name in a class declaration. The object itself is considered opaque, its internal structure is unknown.

Template <class T> accessor make_accessor(T&) returns an accessor for any kind of object.

Here is a reduced version of the accessor interface:

string accessor::to_string()

     returns a string representation of the value of the concrete object

size_t accessor::from_string(string value)

     sets the value of the concrete object from a string

bool accessor::is_aggregate()

     returns true if the concrete object is an aggregate (struct or class)

bool accessor::is_container()

     returns true if the concrete object is a container (container abstraction layer)

bool accessor::is_inherited()

     returns true if the concrete object is a superclass of another concrete object

prop_t accessor::type()

     returns the type of the concrete object

bool accessor::is_type(prop_t aType)

     returns true if the type of the concrete object is ‘aType’

This “Hello World” example of a generic algorithm simply writes the value of an object of an unknown class to the output:

    void example1(accessor a)
    {
       cout << “Hello world: “ << a.to_string() << endl;
    }

The function example1 works with classes whose declaration is unknown at compile time. It is possible to compile example1 and put it into a binary library. To use it, a programmer would write

    void main(int argc, char *argv[])
    {
       accessor a=make_accessor(argc);
       example1(a);
       float f=10.6;
       a=make_accessor(f);
       example1(a);
    }

Accessors hide the actual type of an object from a generic algorithm. But it is often preferable to access the object using its actual type. This is achieved through dynamic casting, which reverses the effect of an accessor. Where an accessor adapter allows untyped access to a concrete object, a typed_accessor<T> allows typed access. The function dynamic_cast_accessor<T> constructs a typed_accessor<T> from an accessor. The typed_accessor<T> will be invalid if the concrete object is not of type T.

typed_accessor<T> is derived from accessor and adds get/set methods accepting objects of type T.

    int foobar=15;
    accessor a=make_accessor(foobar);
       // create a typed accessor from ‘a’
    typed_accessor<int> i=dynamic_cast_accessor<int>(a);
    assert(i.is_valid());
    assert(i.get() == 15);
    i.set(20);
       // i points to foobar, thus foobar must have changed
    assert(foobar==20);

    FooBarStruct aStruct;
    a=make_accessor(aStruct);
    i=dynamic_cast_accessor<int>(a);

‘a’ now points to an object of type ‘FooBarStruct’, so dynamic_cast_accessor<int> cannot return a valid pointer.

    assert(i.is_valid() == false);
    i=dynamic_cast_accessor<int>(a);
    // a does not point to a type int
    assert(i.is_valid() == false);

struct or class – the aggregate class

aggregate adapters handle struct or class definitions. Like accessors they point to a concrete object and expose its properties through a generic interface. Unlike accessors they are able to provide information about the internal structure of the object they point to.

Template <class T> aggregate make_aggregate(T &t) returns an aggregate for a concrete object which must be an aggregate.

Aggregates implement an STL like interface with aggregate::iterator, aggregate::begin() and aggregate::end() to iterate over the member variables of the class or struct they points to. The iterator returns accessor objects (not aggregates). The following example shows some of the details:

void example2(aggregate a)
{
   aggregate::iterator I;
   for (I=a.begin(); I!=a.end(); ++I) {
      cout << “calling example1 for “
           << I->class_name() << “::” << I->name() << endl;
      example1(*I); // *I is an accessor
   }
}

example2 accepts an aggregate adapter that can point to any kind of struct or class. It iterates over all member variables and prints the class name, variable name and the value of each. To use it a programmer would write something like this:

struct FooBarAggregate
{
   int anInt;
   string aString;
};

void main(int argc, char *argv[])
{
   FooBarAggregate a;
   a.anInt = 15;
   a.aString=”this is a string”;
   example2(make_aggregate(a));
}
The output would look similar to:
calling example1 for FooBarAggregate::anInt
Hello world: 15
calling example1 for FooBarAggregate::aString
Hello world: this is a string

Here is a reduced version of the aggregate interface:

aggregate::iterator aggregate::begin()

    return an iterator to the first member variable of the aggregate, end() if there are no member variables

aggregate::iterator aggregate::end()

    return an iterator pointing behind the last member variable of the aggregate

aggregate::iterator aggregate::find(const char* memberName)

    search the member variables of the aggregate for a member with the name ‘memberName’. Return an iterator to this member or end() if no such member exists. Find does not search the C++ inheritance hierarchy.

Aggregate::iterator aggregate::find_scope(const char* memberName)

    searches the member variables of an aggregate using C++ scope rules.

Accessor aggregate::operator[](const char* memberName)

    returns *find_scope(memberName). Throws an exception if no member with that name exists.

Accessor aggregate::get_accessor()

    return an accessor object for this aggregate

find_scope and operator[] search the aggregate using C++ like scope rules, whereas find only searches the current aggregate. Consider

class Base {
   int anInt ;
   int baseInt ;
   string baseString;
};
class Inherited:public Base {
   int anInt;
};

Create an aggregate object for a concrete object of type Inherited.

Inherited foobar;
aggregate a;
a=make_aggregate(foobar);

a.find(“baseInt”) will return end() since Inherited has no member ‘baseInt’. A.find_scope(“baseInt”) uses C++ scope rules and will return an iterator pointing to foobar.Base::baseInt.

a.find(“anInt”) will return an accessor pointing to foobar.anInt. a.find_scope(“anInt”) will also return an accessor for foobar.anInt. find_scope(“Base.anInt”) however will return an accessor for foobar.Base::anInt.

Both find(“Base”) and find_scope(“Base”) will return an accessor for (Base&)foobar.

a[“baseString”].to_string() will return a string representation of foobar.baseString. It is the same as a.find_scope(“baseString”)->to_string().

a[“aMember”] will throw an exception, because there is no member “aMember”.

Container implementations – the container class

The data abstraction layer also hides container implementations. Don’t confuse this with an abstract interface for container and iterator implementations, which is part of the boost library. This has nothing to do with templates.

Like accessor and aggregate adapters, the container adapter lets a programmer access concrete containers whose definition and implementation is not known at compile time. The container adapter is type independent, as the following example demonstrates:

stl::vector<string> myVector;
stl::list<float> floatList;
container c;

c=make_container(myVector); // create a container for myVector
c=make_container(floatList); // create another container for floatList

It first creates a container adapter ‘c’ pointing to the stl::vector<string> object ‘myVector’, then reassigns ‘c’ to a different concrete container stl::list<float> ‘floatList’. I deliberately choose two different container implementations, vector<> and list<> to highlight the fact that the container object ‘c’ is type independent. It works with any type of container and it can be reassigned to a different container implementation.

Like aggregates, containers let a programmer iterate over the internal elements of the concrete container it points to. Where the internal elements of an aggregate are the individual member variables of the struct/class definition, the internal elements of the container are the objects contained in the container.

The interface is similar to the aggregate interface. Begin() and end() return iterators to the first and last+1 element and container::iterator is a type independent iterator for the concrete container. The container::iterator type is designed as an “adapter” design pattern. It adapts the concrete container iterator implementation to a generic implementation that programmers can use in generic algorithms.

Here is a reduced version of the container interface:

container::iterator container::begin()

    return an iterator to the beginning of the container

container::iterator container::end()

    return an iterator to the end of the container

prop_t container::get_element_type()

    returns a type descriptor for the type of the elements of a container

Putting it all together

The following example will fill a list box with elements from a container. The example can be written and compiled and put into a library even when the type of the container or the elements that are to be used is not yet known.

FillListBox takes three parameters: a list box where the elements are to be shown. A data abstraction layer object of type container that points to the actual data and a string telling the function which member variable of each element shall be shown in the list box.

void FillListBox(wxListBox *lb, container c, string selectMember=””)
{
   container::iterator i;
   for (i=c.begin(); i!=c.end(); ++i) {
      accessor current=*i;
      accessor show;
      if (current.is_aggregate() && selectMember!=””) {
         // search for a member ‘selectMember’
         show = current[selectMember];
      } else // ignore ‘select Member’ and show the element instead
         show = current;
      lb->AddString(show.to_string());
   }
}

This function compiles and links without any knowledge of the actual container that is going to be used for the list box.

Here are two different examples using the FillListBox.

struct DefectRecord {
   string subject;
   string description;
   string developer;
   int priority;
};

std::vector<DefectRecord> aDefectList;

FillListBox(listbox, make_container(aDefectList), “subject”) will fill the list box with all entries from the defect list and show the ‘subject’ member variable of each entry.

FillListBox(listbox, make_container(aDefectList), “priority”) will fill the list box, but will display the priority instead. This is just to prove the point that FillListBox can show member variables that are not of type string.

The second example uses a simple string list instead.

    std::list<string> userList;

FillListBox(listbox, make_container(userList)) will fill the list box with the strings from the user list.

Continued with Part 2: creating adapters, coming soon

 

 

[Home] [Products] [Shop] [Knowhow] [Library] [About]

© 2004, Hajo Kirchhoff, last modified Mrz 01, 2009

back to top