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()
|
|
|
|
size_t accessor::from_string(string value)
|
|
|
|
bool accessor::is_aggregate()
|
|
|
|
bool accessor::is_container()
|
|
|
|
bool accessor::is_inherited()
|
|
|
|
prop_t accessor::type()
|
|
|
|
bool accessor::is_type(prop_t 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()
|
|
|
|
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)
|
|
|
|
Accessor aggregate::operator[](const char* memberName)
|
|
|
|
Accessor aggregate::get_accessor()
|
|
|
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()
|
|
|
|
container::iterator container::end()
|
|
|
|
prop_t container::get_element_type()
|
|
|
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
About the author
My name is Hajo Kirchhoff, I
currently live in Germany, but am planning to move to Edinburgh, Scottland, to
join forces with Julian Smart, founder of the wxWidgets UI library (www.wxwidgets.org). I am 37, have a degree
in computer science from the Friedrich-Alexander-Universität in Erlangen and
have been writing software since 1983, when I was doing such fascinating and
pointless things as reverse engineering the UCSD Pascal p-code interpreter for
the Apple II and porting it from the 6502 to a Motorola 68000 CPU and other
silly projects.
Today I work as a
freelance computer specialist. I design and implement applications for my
clients in the telecommunications industry and also do consulting work where I
help teams to come to grips with their software process and finish their
projects on time and on budget.
Last year I began selling
my own products, starting with wxVisualSetup,
a tool for programmers using wxWidgets and MS Visual Studio, and BugLister, a small, lightweight
bug tracking utility and. BugLister 2 is currently under development and will
include requirements management and project tracking features.
|
|
|