© Copyright 2000, Detlef Vollmann
Over the last two years I've held several tutorials on meta-classes and reflection in C++. I use the term reflection here in its original sense, "looking back to oneself", nowadays sometimes called introspection. The more general process to allow modifications at class level at run-time is the old task to provide a meta-level, but is today sometimes (mis-)called "behavioural reflection" or "structural reflection".
In some sense this article presents work in progress, though it's going on now for nearly ten years. It's not the definitive meta-object protocol for C++, but more a presentation of lesser-known C++ techniques to solve some specific design problems.
If you have comments on these techniques, or proposals how the problems could be solved completely differently, or if you find errors in this article, I really appreciate your feedback to dv@vollmann.ch.
This document can be found on the web at http://www.vollmann.com/en/pubs/meta/index.html.
Some source code to illustrate the implementation of the ideas of this article can be found at http://www.vollmann.com/download/mop/index.html.
The code in this article is not completely identical with that source code (due to typographical reasons and compiler restrictions). So, it could well be that some errors crept into the code here; if you find them, please mail me as well.
Book
class:class Book { public: Book(const string & author_, const string & title_, const string & publisher_, double price_, double weight_); string getName() { string name; name = author + ": " + title; return name.substr(0, 40); } double getPrice(); double getWeight(); private: string author, title, publisher; double price, weight; };Your solution works, Susan is happy, and all is fine for a while...
Product
class (Fig. 3): A simple interface
for ShoppingCart
providing
getName()
, getPrice()
, and
getWeight()
. This is what you already have. Then you need a
different interface for a general search
machine2,
which must provide information like:someBook.author
, is
information on the object level. But information about the
properties of the object itself, about its attributes, its
structure, etc. is meta information.Type
.Type
is to distinguish different kind of
Attribute
s. For this, a simple enum
would suffice.
But the idea of types is to have different kind of Values
for
different Type
s, so the Type
should
create new Value
s. So we
put the enum
inside the Type
class,
provide the newValue()
method, and get the interface shown in Fig. 4.Value
for now, if we have different kind of values we probably
need some base class for them. Let's call it "BaseValue
",
and newValue()
can just return a pointer to BaseValue
.static vector
3.class Type { public: enum TypeT {stringT, intT, doubleT, unknownT}; explicit Type(TypeT typeId_) : typeId(typeId_) {} BaseValue * newValue() const { return prototypes[typeId]->clone(); } TypeT getType() const { return typeId; } static void init(); { prototypes[stringT] = new Value<string>(""); prototypes[intT] = new Value<int>(0); prototypes[doubleT] = new Value<double>(0); } private: TypeT typeId; static vector<BaseValue *> prototypes; }; vector<BaseValue *> Type::prototypes(Type::unknownT);
Value
s through Type
,
Attribute
contains only a name and type, so here is its
implementation:class Attribute { public: Attribute(const string & name_, Type::TypeT typeId) : name(name_), type_(typeId) {} const string & getName() const { return name; } Type getType() const { return type_; } private: string name; Type type_; };
Class
itself. As multiple inheritance is probably
not an issue for our purposes, we have just one pointer to a
base class (which can be 0). The more important question is the
attribute list: Should it hold only the attributes defined for
this class or should it include all inherited attributes? While
for the actual object value access a complete list is more
useful (and much faster), for class maintenance it might be
important to know which attribute was defined in which class.
So we just keep both. For the complete list, the order might be
significant: should the own attributes come first or the inherited
ones? In most illustrations the inherited attributes come
first, so we keep this order as well.findAttribute()
must do a
reverse search. What
shall we return from findAttribute()
? The STL way would
be to return an iterator, but for applications with GUIs (to
create new objects and assign values to its attributes based on
selection lists) an index-based access to the attributes will
be more appropriate. So findAttribute()
returns an index and
getAttribute()
takes an index and returns an
Attribute
. So the
Attribute
lists need to be an indexed containers, so we
choose vector
s for them.Class
is to create
Object
s from it, so it
has a method newObject()
which returns a pointer to an Object
.
Do we need to keep a repository with references to all created
objects? For a full reflection interface we should do this. But
for actual applications this is nearly never useful, as objects
of the same class are created for completely different
purposes. But do we need the repository for internal use? It
depends on what we want to do with objects after they were
created. This leads directly to another important decision:
What do we do with already existing objects if we add a new
attribute to a class? One option is to add this attribute to
all existing objects and assign it a default value. The other
option is to leave these existing objects and add the new
attribute only to new objects. This leads to differently
structured objects of the same class at the same time, and then
we must add some version information to the objects. But there
is a third option: To forbid the modification of a class
definition once an instance of that class was created. This is
the easiest option, so we adopt it for our MOP and add a flag
definitionFix
. With that flag, we can skip the object
repository.addAttribute()
.class ClassDef { //typedefs Container, Iterator for attributes public: ClassDef(ClassDef const * base, const string & name_) : baseClass(base), name(name_), definitionFix(false) { baseInit(); effectiveAttributes.insert(effectiveAttributes.end(), ownAttributes.begin(), ownAttributes.end()); } template <typename iterator> ClassDef(ClassDef const * base, const string & name_, iterator attribBegin, iterator attribEnd) : baseClass(base), name(name_), ownAttributes(attribBegin, attribEnd), definitionFix(false) { baseInit(); effectiveAttributes.insert(effectiveAttributes.end(), ownAttributes.begin(), ownAttributes.end()); } string getName() const; Object * newObject() const { definitionFix = true; return new Object(this); } AttrIterator attribBegin() const; AttrIterator attribEnd() const; Attribute const & getAttribute(size_t idx) const; void addAttribute(const Attribute &); size_t getAttributeCount() const; size_t findAttribute(string const & name) const { // this does a reverse search to find the most derived AttributeContainer::const_reverse_iterator i; for (i = effectiveAttributes.rbegin(); i != effectiveAttributes.rend(); ++i) { if (i->getName() == name) { return distance(i, effectiveAttributes.rend()) - 1; } } return getAttributeCount(); } private: void baseInit() { if (baseClass) { baseClass->definitionFix = true; copy(baseClass->attribBegin(), baseClass->attribEnd(), back_inserter<AttributeContainer>(effectiveAttributes)); } } ClassDef const * const baseClass; string name; AttributeContainer ownAttributes, effectiveAttributes; mutable bool definitionFix; };
Object
,
we have to think about Value
.
We need a common interface to manage them, which we already
called BaseValue
. But what interface do we need? The whole idea
of Value
is to store values,
so we need a set()
function. What
parameter? The only thing we have is BaseValue
, so that's the
parameter type. Pass by value, by reference, or by pointer?
Definitely not by value, as BaseValue
is only an interface. On
the other hand, what you pass is a value, so the parameter
passing should be by value to let you pass temporaries. So one
option would be to pass by const reference. But though this
helps for the problem at hand, it doesn't cure the fundamental
problem: you should have a value, but all you have is a
polymorphic interface. The real solution here is the pimpl
idiom, also known as Cheshire Cat,
Envelope/Letter, or more
generally Handle/Body. So we add a handle class, name it
Value
, and look at it later again. For now we're still at
BaseValue
.set(Value)
, so what about get()
?
The return
type of get()
would be Value
, and the
implementation would look
like:Value BaseValue::get() { return *this; // calls Value(BaseValue const &) }But that we can do directly, so
get()
doesn't make much
sense.BaseValue
functions do we need? Values must be
copied, so we add clone()
.asString()
for convenience5.class BaseValue { public: virtual ~BaseValue(){} virtual BaseValue * clone() const = 0; virtual string asString() const = 0; // fromString() virtual void set(Value const & v) = 0; // no get()! private: // Type info };
int
,
double
, string
, ....
And an int
value must hold an int
,
a double
value a double
,
etc. This looks like an opportunity for a template. So, let's
define RealValue<T>
, derive it from
BaseValue
, implement
the inherited interface, and we're nearly done. But as
RealValue<T>
is just a wrapper around
T
with some
additional functionality, but essentially still a T
, we should
provide conversion in both directions, by providing a
converting constructor and a conversion operator.template <typename PlainT> class RealValue : public BaseValue { public: RealValue(PlainT v) : val(v) {} RealValue * clone() const { return new RealValue(*this); } string asString() const { ostringstream os; os << val; return os.str(); } operator PlainT() const // conversion to plain type { return val; } RealValue<PlainT>::set(Value const & v) { val = v.get<PlainT>(); } private: PlainT val; };A note about
RealValue
: As we have conversion in both
directions, we can use RealValue<T>
like T
:RealValue<int> i = 1; int j = i; RealValue<double> d = i + 5.2 / (i*2); cout << d << endl;Nearly: the following doesn't work:
RealValue<string> name, author = "Bjarne", title = "The C++ PL"; name = author + ": " + title; cout << name << endl;The reason is that the compiler only applies one user-defined conversion, but for string literals, you need two: from
char const *
to string
,
and from string
to
RealValue<string>
. If you want to work with
RealValue<string>
outside the MOP, you should define a
specialization:template <> class RealValue<string> : public BaseValue, public string { public: RealValue(string const & s) : string(s) {} RealValue(char const * s) : string(s) {} RealValue() {} RealValue * clone() const { return new RealValue(*this); } string asString() const { return static_cast<string>(*this); } // no operator string(), conversion to base automatically void set(Value const & v) { string::operator=(v.get<string>()); } };Note: Actually, its not really clean to derive
RealValue<string>
from std::string
,
but as long as you
don't delete a RealValue<string>
through a pointer to
string
, it will work.Value
. As a handle class, it
contains its body and cares for it. Its main job is to
adopt/create and to delete its body. And it mirrors the
interface of the body and forwards all messages. But it should
also be a real value class, thus providing default and copy
constructor and assignment. But how to implement the default
constructor? As we don't know what type to create, we
must create an empty handle without a body and check before
forwarding if we actually have something to forward to. The
assignment is essentially the set()
,
so we skip the set()
.get()
. Of course, to return
a Value
or BaseValue
doesn't make sense.
But what about
returning the RealValue
or even the wrapped underlying value?
That would be really useful, but for that we have to tell get()
what we want as return type. So get()
becomes a member template
and so can return whatever is inside the RealValue<>
.class Value // Value handle { public: Value(BaseValue const & bv) : v(bv.clone()) {} Value(Value const & rhs) : v(rhs.v ? rhs.v->clone() : 0) {} explicit Value(BaseValue * bv = 0) : v(bv) {} ~Value() { delete v; } Value & operator=(const Value & rhs) { // this is not a typical pimpl assignment, but a set() if (v) { if (rhs.v) { // fine, all v's exist v->set(rhs); } else { // the other v doesn't exist, so we must delete our own :-( BaseValue * old = v; v = 0; delete old; } } else { // we don't have a v, so just copy the other v = (rhs.v ? rhs.v->clone() : 0); } return *this; } template <typename PlainT> PlainT get() const { if (v) { RealValue<PlainT> const & rv = dynamic_cast<RealValue<PlainT> const &>(*v); return rv; // uses conversion operator } else { return PlainT(); } } std::string asString() const { if (v) { return v->asString(); } else { return string(); } } private: BaseValue * v; };
Object
. Now, as we have everything else,
an Object
is mainly a container for its attribute values. To
ease implementation, we will structurally mirror the attribute
container in the class definition, so we use a vector
.
As we have so
much effort invested in our Value
handle, it
would make sense to store that in the vector
.
But for future extensions it will be easier to have the
BaseValue
pointers directly available.ClassDef*
.class Object { public: explicit Object(ClassDef const * class_) : myClass(class_), values(class_->getAttributeCount()) { buildValueList(); } ClassDef const & instanceOf() const { return *myClass; } Value getValue(size_t attribIdx) const { return *values[attribIdx]; // calls Value(BaseValue &) } Value getValue(string const & attribName) const { size_t idx = instanceOf()->findAttribute(attribName); // should check for not found return getValue(idx); } void setValue(size_t idx, Value const & v) { values[idx]->set(v); } void setValue(string const & attribName, Value const &v) { size_t idx = instanceOf()->findAttribute(attribName); // should check for not found setValue(idx, v); } private: typedef vector<BaseValue *> ValueContainer; void buildValueList() { ClassDef::AttrIterator a; ValueContainer::iterator i = values.begin(); for (a = instanceOf()->attribBegin(); a != instanceOf()->attribEnd(); ++a, ++i) { *i = a->getType().newValue(); } } ClassDef const * const myClass; ValueContainer values; };Now the MOP is complete. Let's use it:
Product
class:ClassDef * product = new ClassDef(0, // no base class for Product "Product"); // name of classAdding attributes:
product->addAttribute(Attribute("Product Number", Type::intT)); product->addAttribute(Attribute("Name", Type::stringT)); product->addAttribute(Attribute("Price", Type::doubleT)); product->addAttribute(Attribute("Weight", Type::doubleT));Creating the
Book
class with an attribute list:list<Attribute> attrL; attrL.push_back(Attribute("Author", Type::stringT)); attrL.push_back(Attribute("Title", Type::stringT)); attrL.push_back(Attribute("ISBN", Type::intT)); ClassDef * book = new ClassDef(product, // base class "Book", attrL.begin(), attrL.end());Creating an object:
Object * bscpp(book->newObject());
ProductNo
):bscpp->setValue(0, RealValue<int>(12345));Same for a string value:
bscpp->setValue(4, RealValue<string>("Bjarne Stroustrup"));Better way: set value by name this gives the most derived attribute:
bscpp->setValue("Title", RealValue<string>("The C++ Programming Language")); bscpp->setValue("Weight", Value<double>(370));
ClassDef::AttrIterator a; size_t idx; for (a = book->attribBegin(), idx = 0; a != book->attribEnd(); ++a, ++idx) { cout << a->getName() << ": " << bscpp->getValue(idx).asString() << endl; }and we get:
Product Number: 12345 Name: Price: Weight: 370 Author: Bjarne Stroustrup Title: The C++ Programming Language ISBN:So, our MOP is complete. For our sample application, you have to add a class repository, some nice GUI to define classes and objects, creating the index for the search machine, provide an interface for
ShoppingCart
, but then you're done, and Susan
is happy as she now can create her own new product categories
at runtime.
ShoppingCart
in our
example, you'll find that it isn't so easy. If all
classes are dynamic classes, all access must go through the
MOP:getName()
for Book
:string bookGetName(Object const * book) { if (book->instanceOf().getName() != "Book") { /* throw some exception */ } string name; // name = book->author + ": " + book->title; it was so easy... string author = book->getValue("Author").get<string>(); string title = book->getValue("Title").get<string>(); name = author + ": " + title; return name.substr(0, 40); }For a lot of applications, it would be useful to provide some classes of a hierarchy as C++ classes, e.g.
Product
,
but still let the
user add classes of the same hierarchy at runtime, e.g. TShirt
.
So,
let's look at this. If we want access through our MOP to
C++ classes, we need a getValue()
to which we can give the
attribute we want to access at runtime. So, here it is:Value getValue(Object *o, MemberPointer mp) { return o->*mp; }The magic lies in
'->*'
: This is the
pointer-to-member selector of C++.class Product { // ... protected: RealValue<double> price; }; class Book : public Product { public: // ... private: RealValue<string> author, title; RealValue<double> weight; }; Book b, *bp;A pointer-to-member is a type that is derived from two other types: The type of the base object (
Book
in our example) and
the type of the member (RealValue<>
). The type decorator
for a pointer-to-member is '::*
', so let's
define two variables with initialization:RealValue<string> Book::* bookStringMemPtr = &Book::author;
RealValue<double> Book::* bookDoubleMemPtr = &Book::weight;
The pointer-to-member selector comes in two variations: as
'.*
' you can apply it to references of the class
and as '->*
' it takes a pointer. It is a binary
operand, as left operand it takes a reference (or pointer) to
an object and as right operand a pointer-to-member. So, with
the above definitions, you can do things likeb.*bookStringMemPtr = "Bjarne Stroustrup"; // assigns b.author bookStringMemPtr = &Book::title; bp->*bookStringMemPtr = "The C++ Programming Language"; // assigns b.authorOf course, as
title
is a private member of Book
, the
assignment of the pointer-to-member must be at the scope of
that class. But the pointer-to-members themselves can be used
even if you don't have access privileges to the members
(as long as you have access to the pointer-to-member).RealValue<double> Book::*
,
RealValue<double> Product::*
,
and BaseValue Product::*
are different types. But are there conversions? The C++
standard provides a conversion from a pointer-to-member of a
base class to a pointer-to-member of a derived class. That
makes sense: You can apply an offset to a member of a base
class to the base address of a derived object as well, as the
base is a part of the derived
object7.
So you can assignbookDoubleMemPtr = &Product::price;
as the price
is part of each Book
instance.RealValue<double> Product::* &Book::author;
as author
is not a member of each instance of type
Product
.RealValue<double> Book::*
to
BaseValue Book::*
, though it
would be save: If the result type of the pointer-to-member
selector is a reference to a derived class, it can be safely
converted to a reference of a respective base class, so it
would also be safe to let the compiler do the conversion
automatically and therefore also convert the pointer-to-members
themselves. As already mentioned, the standard doesn't
provide (implicit) and even doesn't allow (explicit through
static_cast
) that conversion, probably
because the committee didn't see any use for
pointer-to-data-members at all (see
[5]), and for
pointer-to-member-functions that conversion really
doesn't make sense.reinterpret_cast
, but the only
thing you can safely do with a reinterpret_cast
ed thing is to
reinterpret_cast
it back, and for that you have to store the
original type as well. So we use another option: we just define
the conversion! But as C++ doesn't allow you to define
your own conversions to compiler-provided types (and in this
sense the pointer-to-members are compiler defined, though the
involved single types like Book
or
BaseValue
are user-defined),
we have to define wrapper classes around them.template <typename BaseType, typename BaseTargetType> class MemPtrBase { public: virtual BaseTargetType & value(BaseType & obj) const = 0; virtual BaseTargetType const & value(BaseType const & obj) const = 0; protected: MemPtrBase() {} virtual ~MemPtrBase() {}; private: MemPtrBase(MemPtrBase const &); MemPtrBase & operator=(MemPtrBase const &); }; template <typename BaseType, typename BaseTargetType, typename TargetType> class TypedMemPtr : public MemPtrBase<BaseType, BaseTargetType> { public: TypedMemPtr(TargetType BaseType::* ptr) : p(ptr) {} BaseTargetType & value(BaseType & obj) const { return obj.*p; } BaseTargetType const & value(BaseType const & obj) const { return obj.*p; } private: TargetType BaseType::* p; }; template <typename BaseType, typename BaseTargetType> class MemPtr // this is a handle only { public: template <typename BaseType2, typename TargetType> explicit MemPtr(TargetType BaseType2::* ptr) : p(new TypedMemPtr<BaseType, BaseTargetType, TargetType>(static_cast<TargetType BaseType::*>(ptr))) {} ~MemPtr() { delete p; } BaseTargetType & value(BaseType & obj) const { return p->value(obj); } BaseTargetType const & value(BaseType const & obj) const { return p->value(obj); } private: MemPtrBase<BaseType, BaseTargetType> * p; };Some notes to the code:
BaseType
is used for the class to
which a pointer to member is applied (e.g. Book
),
TargetType
is
the result type to which a pointer-to-member points
(RealValue<double>
), and
BaseTargetType
is the base class
of TargetType
(BaseValue
).
MemPtrBase<>
is the common
base class as we need it (e.g.
MemPtrBase<Book, BaseValue>
,
which stands for BaseValue Book::*
),
TypedMemPtr<>
hold an actual C++ pointer-to-member
(TypedMemPtr<Book, RealValue<double> >
),
and
MemPtr<>
is a
handle class around MemPtrBase<>
to
store them in a container. Here, the actual access function is
the value()
member function. If you want, you can add a
global operator '->*
' (as template function), but you
can't provide the operator by a member function (as the
left operand is not the class instance),
and you can't overload '.*
' (this is one of
the few non-overloadable operators).MemPtr
constructor is a member template with two
template parameters: a BaseType2
and the TargetType
. The second
one is clear as it defines the actual TypedMemPtr
to be
constructed, but the BaseType2
is not so obvious. If we omit
the BaseType2
, so only havingtemplate <typename BaseType, typename BaseTargetType> class MemPtr // this is a handle only { public: template <typename TargetType> explicit MemPtr(TargetType BaseType::* ptr) : p(new TypedMemPtr<BaseType, BaseTargetType, TargetType>(ptr)) {} // ... }and then we try to create a
MemPtr<Book, BaseValue> mp2(&Product::price);some compilers give an error, as they cannot fiddle out the correct conversion. This would be to convert
RealValue<double> Product::*
to
RealValue<double> Book::*
, which should be done
automatically, and then to instantiate MemPtr
's
constructor with RealValue<double>
as TargetType
.MemPtr<Book, BaseValue> mp2(static_cast<RealValue<double> Book::*>(&Product::price));but that's quite a lot to type. It's actually much easier to move that explicit conversion into the constructor itself and just provide an additional template parameter, as shown in the implementation above. The compiler checks the conversion anyway, so you will get a compile time error if that conversion is not allowed (e.g. if you try to convert a
RealValue<double> Book::*
to
RealValue<double> Cd::*
.MemPtr
s allow you to access the attribute values of an
ordinary C++ object. This helps for one part of the MOP. But
what about the attributes themselves? The compiler has the
necessary knowledge, but unfortunately there is no standard way
to access that knowledge at runtime. So we must provide it and
define an interface for it. To allow a smooth integration with
our existing ClassDef
,
we provide an Attribute
iterator as
interface. For now, we provide the information about the
attributes manually, but in a following article we'll
explore the use of a pre-processor for that.Book
we provide the following
functions:class Book : public Product { typedef MemPtr<Book, FinalValue> MemberPtr; public: // as before static size_t ownAttribCount() { return 5; } static Attribute * ownAttribBegin() { static Attribute a[] = {Attribute("Author", Type::stringT), Attribute("Title", Type::stringT), Attribute("Publisher", Type::stringT), Attribute("Price", Type::doubleT), Attribute("Weight", Type::doubleT) }; return a; } static Attribute * ownAttribEnd() { return ownAttribBegin() + ownAttribCount(); } static MemberPtr * memberBegin() { static MemberPtr m[] = {MemberPtr(&Book::productNo), MemberPtr(&Product::weight), MemberPtr(&Book::author), MemberPtr(&Book::title), MemberPtr(&Book::publisher), MemberPtr(&Book::price), MemberPtr(&Book::weight) }; return m; } static MemberPtr * memberEnd() { return memberBegin() + 7; } private: RealValue<string> author, title, publisher; RealValue<double> price, weight; };Please note the difference between
ownAttribBegin()
and memberBegin()
:
the first provides information only about the own
attributes, while the latter provides access also to
base class members. This separation makes sense:
while on object level all data members build together one object,
on the class level the base class is a different entity
and should be available as common base class for the meta
object protocol as well.
But this separation has consequences: we can't derive a C++
class from a MOP class (but this is definitely no
real restriction) and the C++ base class must be also
made known to the MOP (but that's useful anyway).baseClass
in ClassDef
must be made a ClassDef
instance as well, as noted above.
The C++ base classes are not of much use for our
application.ClassDef
from a C++
class; it's so easy that we can even provide a helper
function for that:template <typename CppClass> ClassDef makeClass(ClassDef const * base, string const & name) { return ClassDef(base, name, CppClass::ownAttribBegin(), CppClass::ownAttribEnd()); }Now, we can build our
ClassDef
s for
Product
and Book
and
create instances from them:ClassDef base(makeClass<DynaProduct>(0, "Product")); ClassDef book(makeClass<Book>(base, "Book")); book.newObject();But stop -- though this works, it's not what we want: now, the instances are not genuine C++ objects, but MOP objects, and all access must still go through the MOP.
Object
interface. The OO way
to do that is to derive our Product
from Object
, but though
other OO languages like Smalltalk do that, I think
there's a better, less intrusive option: we provide an
adaptor.Object
and containing a Product
member or using multiple inheritance and derive the adaptor
from Object
and Product
.
For simplicity, we will use the first
option, but real world applications often benefit from the
second approach. So we provide a wrapper class CppObject
that
is derived from Object
and that holds the original C++ object.
It implements the interface of Object
(getValue()
and
setValue()
) through our MemPtr
s:template <typename OrigClass> class CppObject : public Object { typedef MemPtr<OrigClass, BaseValue> MemberPtr; public: CppObject(ClassDef const * myClass) : Object(myClass), myObject(), members(OrigClass::memberBegin()) {} virtual Object * clone() const { return new CppObject(*this); } using Object::getValue; // importing getValue(name) using Object::setValue; // importing setValue(name) virtual Value getValue(size_t idx) const { return members[idx].value(myObject); } virtual void setValue(size_t idx, Value const & v) { BaseValue * p = &(members[idx].value(myObject)); p->set(v); } private: MemberPtr * members; OrigClass myObject; };A useful rule of OO design says that only leaf classes should be concrete, so let's define
Object
as abstract
base class and create a new class DynaObject
that resembles our
former Object
for real MOP class instances (Fig. 7).prod.newObject()
, we still get
it wrong: we now get a DynaObject
, but we want a
CppObject<Product>
. To solve that, we must provide the
ClassDef
with a means to create the correct kind of object, and
the simplest way to do that is a factory method: we provide a
static creation function in CppObject<>
and DynaObject
,
give a pointer to that function to the ClassDef
's
constructor, store it and use that function in
ClassDef::newObject()
:DynaObject
and
CppObject
:Object * DynaObject::newObject(ClassDef const * myClass) { return new DynaObject(myClass); } template <typename OrigClass> Object * CppObject<OrigClass>::newObject(ClassDef const * myClass) { return new CppObject(myClass); }Changes to
ClassDef
:
class ClassDef { public: typedef Object * (*CreateObjFunc)(ClassDef const *); template <typename Iterator> ClassDef(ClassDef const *, string const &, CreateObjFunc objFunc, Iterator, Iterator) : // ... createObj(objFunc) { // ... } ClassDef(ClassDef const *, string & const name_, CreateObjFunc objFunc) : // ... createObj(objFunc) { // ... } Object * newObject() const { definitionFix = true; return (*createObj)(this); } // ... as before private: const CreateObjFunc createObj; // ... as before };And a simple change to
makeClass
:template <typename CppClass> ClassDef makeClass(ClassDef const * base, string const & name) { return ClassDef(base, name, CppObject<CppClass>::newObject, CppClass::ownAttribBegin(), CppClass::ownAttribEnd()); }Now, everything works. Well -- nearly. If we now try to create a
ClassDef
for Product
with makeClass
the compiler
complains about creating an abstract class: makeClass
gives the
ClassDef
constructor a pointer to
CppObject<Product>::newObject()
, and that creates a
Product
instance as part of
CppObject<Product>
. This is
easily fixed: just call the ClassDef
constructor directly with
a null-pointer for the creation function, thus prohibiting the
creation of a Product instance through the MOP.ClassDef base(0, "Product", 0, Product::ownAttribBegin(), Product::ownAttribEnd()); ClassDef book(makeClass<Book>(&base, "Book"));You can create instances of them
book.newObject();
Product
ClassDef * tShirt = new ClassDef(&base, "T-Shirt", DynaObject::newObject); tShirt->addAttribute(Attribute("Size", Type::stringT)); tShirt->addAttribute(Attribute("Color", Type::stringT)); tShirt->addAttribute(Attribute("Name", Type::stringT)); tShirt->addAttribute(Attribute("Price", Type::doubleT)); classReg.registerClass(tShirt);and manipulate instances of existing classes and new classes through the MOP:
Object * ecpp(book.newObject()); ecpp->setValue(5, RealValue<double>(22.50)); ecpp->setValue(0, RealValue<int>(23456)); ecpp->setValue(2, RealValue<string>("Scott Meyers")); ecpp->setValue("Title", RealValue<string>("Effective C++")); ecpp->setValue(6, RealValue<double>(280)); size_t idx; cout << "ecpp:" << endl; for (a = book.attribBegin(), idx = 0; a != book.attribEnd(); ++a, ++idx) { cout << a->getName() << ": " << ecpp->getValue(idx).asString() << endl; } cout << ecpp->getValue("Author").asString() << endl;And a dynamic object:
Object * ts(tShirt.newObject()); ts->setValue(0, RealValue<int>(87654)); ts->setValue(2, RealValue<string>("XXL")); ts->setValue("Color", RealValue<string>("red")); ts->setValue("Price", RealValue<double>(25.95)); ts->setValue("Weight", RealValue<double>(387)); for (size_t idx = 0; idx != 4; ++idx) { cout << ts->getValue(idx).asString() << endl; }
Product
interface. For instances of C++ classes you
can modify the CppObject<T>
to derive from T
as we have
discussed before, or better, to provide a member function in
CppObject
that returns a pointer to myObject
.
And for instances of MOP classes you can
define a wrapper around an Object
that implements the Product
interface:class DynaProduct : public Product { public: DynaProduct(Object const * o) : obj(o) {} virtual std::string getName() const { Value v = obj->getValue("Name"); return v.get<std::string>(); } virtual double getPrice() const { Value v = obj->getValue("Price"); return v.get<double>(); } virtual double getWeight() const { Value v = obj->getValue("Weight"); return v.get<double>(); } private: Object const * const obj; };And you can't access normal C++ instances of
Book
through the MOP; to solve this, you could add another
constructor that adopts the C++ instance by copying (and
consequently you should delete the original one to avoid an
object that exists twice) or you could modify the
wrapper to hold only a pointer to the C++ object and add a
member function to return the controlled C++ object on
request:
Here, we use the first approach:template <typename OrigClass> CppObject<OrigClass>::CppObject(ClassDef const * myClass, OrigClass const & obj) : Object(myClass), myObject(obj), // calls the copy-ctor of OrigClass, which must be accessible members(OrigClass::memberBegin()) {}And now, you can do this:
Book b("Bjarne Stroustrup", "The C++ Programming Language", "Addison-Wesley", 27.50, 370); CppObject<Book> mb(*book, b); Object * ob = &mb; cout << "C++ object through MOP" << endl; for (a = ob->instanceOf()->attribBegin(), idx = 0; a != ob->instanceOf()->attribEnd(); ++a, ++idx) { cout << a->getName() << ": " << ob->getValue(idx).asString() << endl; }But in general, it's just important that you can define basic classes in C++ at programming time but allow the user to derive own classes from these base classes at runtime.
offsetOf
macro that is still
in widespread use for that purpose.Could the same kind of reflection be achieved with
get
/set
functions instead of pointers-to-members? Perhaps, yes. Some
component mechanisms use that approach (e.g. Borland's
VCL). In that case, a get()
and
set()
function for each
attribute and a specialized TypedMemPtr<>
for each
attribute for each class is required. That's a lot of
work, but with a respective pre-processor or compiler support
that's not a point. But it's still much more
intrusive to add all the getters and setters; and if they are
public, they break encapsulation. Though pointers-to-members
allow direct access to the data, that encapsulation leak can be
much better controlled.
The second remark relates to RealValue<>
. Are they
really necessary for true reflection purpose? Actually not. In
that case, you could remove BaseTargetType
from all MemPtr
templates, return the TargetType
in TypedMemPtr
's getter
function, and make the getter function of the MemPtr
handle a
member template function analogous to the getter function of
Value
. In the source code for this article
(
http://www.vollmann.com/download/mop/index.html) you'll
find a sample implementation for that.
Though this seems to be a major advantage for pure
reflection applications like persistency libraries, in fact I
found it in most cases quite useful to have a base class like
DbValue
for all persistent attributes to provide additional
functionality like dirty flags, type conversion specifics,
etc.
[1] Gregor Kiczales, Jim des Rivières, Daniel G. Bobrow: "The Art of the Metaobject Protocol", MIT Press 1991, ISBN 0-262-61074-4
[2] James O. Coplien: "Advanced C++ Programming Styles And Idioms", Chapters 8-10, Addison-Wesley, 1992, ISBN 0-201-54855-0
[3] Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, Michael Stal: "Pattern-Oriented Software Architecture: A System of Patterns", Wiley 1996, ISBN 0-471-95869-7
[4] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: "Design Patterns", Addison-Wesley 1994, ISBN 0-201-63361-2
[5] Michael S. Ball, Stephen D. Clamage: "Pointers-to-Members", C++ Report 11(10):8-12, Nov/Dec 1999
class
is a reserved word in C++, and I don't like
identifiers to differ only in case from others I have chosen
ClassDef
here.fromString()
would also be useful, but we omit it here.