Mirror reflection library 0.5.13
|
This section discusses several of these problematic situations and how reflection can be of help, with some examples how things can be implemented with Mirror.
There are applications which need to get and then somehow process the name of a given type. One example is simple logging where some of the messages may contain names of types. This is very helpful for example if one wants to log something in a template function and wants to distinguish between various instantiations in the logs. In this case it is very useful if the type name that goes into the log is human-readable and preferrably corresponding to the typename used in the source code. A rather naive implementation could look as follows:
#include <iostream> ::std::ostream& log = ::std::cerr; using ::std::endl; // a template function that returns the name // of the passed type template < typename T > const char* get_typename(void); template < typename A, typename B, typename C > void foo(A a, B b, C c) { log << "entering foo(" << get_typename<A>() << ", " << get_typename<B>() << ", " << get_typename<C>() << ")" << endl; // // do something useful here // log << "leaving foo(" << get_typename<A>() << ", " << get_typename<B>() << ", " << get_typename<C>() << ")" << endl; } int main(void) { foo('A', "B", 0xC); foo(0xAL, ::std::string("B"), L'C'); return 0; }
Maybe the first, obvious choice if one needs to find out what the name of a given type is is to use the typeid
operator and call the name()
function on the returned reference to type_info
. The implementation of our get_typename<T>()
function from the previous sample code could be following:
#include <typeinfo> // a template function that returns the name // of the passed type template < typename T > const char* get_typename(void) { return typeid(T).name(); }
There are however, several problems with is approach. The notoriously known issue of type_info::name()
is that the string returned by this function is implementation-defined and compilers are free to return anything from an empty string, through a mangled typename to a correctly formatted typename and many of them enjoy this freedom to its full extent. Thus the returned name is not guarenteed to be unique nor human readable or easily understandable, nor is it portable. Some compilers provide functions that demangle the names returned by type_info::name()
, but again this is not very portable.
To complicate things even more, we might want to get the base name of the type without the nested-name-specifier (basic_string
vs. ::std::basic_string
) and to get the nested-name-specifier or even the individual names of the enclosing namespaces or classes separatelly.
This is when Mirror comes in handy. One of the basic facilities is the meta_type
class template. Among other things this template has two member functions - base_name
and full_name
that return the base type name without the nested name specifier and the full type name with the nested name specifier respectively. The names returned by these functions correspond to the C++ typenames, thus are human-readable, unique (if using the full_name member function) and portable. There is also a local_name
member function which allows to strip parts of the nested name specifier based on the use of the Mirror's MIRROR_USING_NAMESPACE
macro.
Using meta_type
our get_typename<T>()
function could look like this:
#include <mirror/meta_type.hpp> template < typename T > const char* get_typename(void) { // The MIRRORED_TYPE macro expands into // the proper specialization of meta_type // reflecting the passed type return MIRRORED_TYPE(T)::full_name().c_str(); }
If you want to strip only selected parts of the full type name, you can use the local_name
instead of full_name
and to specify the namespaces to be stripped by using the MIRROR_USING_NAMESPACE
macro.
#include <mirror/meta_type.hpp> template < typename T > const char* get_typename(void) { // this tells the local_name function to strip the std:: // prefix from all full type names MIRROR_USING_NAMESPACE(std); // this tells the local_name function to strip the myproject:: // prefix from all full type names MIRROR_USING_NAMESPACE(myproject); // this is similar to the previous implementation return MIRRORED_TYPE(T)::local_name().c_str(); }
With native C++ types and some common types from the STL and from Boost the meta_type
works out of the box.
There is a large group of operations which work on the structure of different (possibly elaborated) types in a program or on instances of these types in the same way and are generally related to converting the instances of various types to an external representation.
This includes serialization (or saving and loading) of instances of various types to some external data format (XML, JSON, XDR, ASN.1, etc.), marshalling i.e. transforming function parameters during remote procedure calls to a format suitable for transport over network and restoring them on the remote side where the actual call takes place, etc.
Such operations are generally pair-wise, one for converting native C++ objects to and the other to convert them from the external format. It is often desirable to have the ability to do conversions of the objects of the same types to different representations.
One of the traditional ways is to define a common interface for such types and to define a pair of member functions for every external format, one for saving and one for loading.
This means that for N different operations one needs to add 2*N member functions to the interface (for example save_to_xml/load_from_xml, save_to_json / load_from_json, etc.). Furthermore if there are M classes which we want to be persistent we would need to implement 2*N*M member functions related only to serialization. For large projects with many classes and multiple serialization / marshalling formats the number of member functions starts to grow very fast.
For every new serialization format to be added one needs to add 2 functions to the persistence interface and to implement 2*M member functions in the persistent classes. To add a new persistent class one needs to implement 2*N member functions.
It is clear that a large project using this approach will get unmaintainable and full of bugs over time. On the other hand if a reflection facility able to traverse through the member variables of a class and to provide meta-data about them, then it is simple do decouple the persistence-related operations from the types they work on.
Every elaborated type is directly or indirectly composed out of base level types. If we want to save an instance of a class into an XML document, we use reflection to obtain the meta-data describing the instance, the variable used to access it and the class of the instance. Thus we know its memory address, its symbolic name, the internal structure of the class, its member variables, their names and types. If the member variable types are also structured we can obtain meta-data about them recursively until we reach the C++ intrinsic data-type level. The set of these basic data-types is limited (the number is less than 20).
Now all we need to know inside the persistence-related meta-operation is what to do with the native data type instances. In this example how to save them to an XML document, and also how handle structured parts of the class when provided with the meta-data by the reflection facility. This same approach is used also for loading the data from the external representation.
When using this approach in a custom reflection facility without native reflection support, one needs to register the M classes with the facility. To add a new persistence-related operation you need to implement functions for handling the (limited set of) native types.
So for M classes and N persistent data formats one needs to register these M classes with the reflection facility plus to implement 2*N persistent-format-handlers instead of 2*M*N member functions. The advantages of this approach are obvious; the classes are separated from the persistence-related operations. To make a class persistent, one needs only to make it reflectible. Once the meta-data is available any of the persistence-related operations can be executed on instances of that class. To add a new operation it is necessary to implement the interface for meta-operations. Once it is done, the operation can be executed on any object for which meta-data is available. Now the addition of a new class does not depend on the number of the operations and the addition of a new operation does not depend on the number of classes.
Another example of reflection usage may be the problem of implementing object-relational mapping.
This includes generation of relational database schema (tables, their columns, data types, etc.) for storing data of an object-oriented application. The schema should reflect the structure of the classes defined by the application. If a class C has member variables x, y and z, so should the table used to store instances of this class. Furthermore the names of the tables and their columns should be equivalent to the names of the classes and their member variables to avoid confusion.
As the application evolves so should the database schema and this evolution should be automatic or require as less developer attention as possible.
Meta-data can be used to generate GUI components which when passed an instance and the required meta-data can show the layout of the class and also the values of its member attributes
An instance of a service class can be registered inside of some implementation of the RPC/RMI (Remote Procedure Call / Remote Method Invocation) mechanism using the meta-data describing the methods of that service class. This metadata can be then used on the server side to find and invoke the appropriate method of the instance and also as already mentioned the meta-data can be used to convert the parameters and the return value to a form suitable for transport between the address spaces of the client and server processes.
Reflection can be also useful in implementing scripting support for applications. Actually this is very similar to remote calls with the difference that the caller is our scripting language parser or interpreter and it again uses some service object registry and the meta-data to find the proper instance and method, convert the parameters from the script code to suitable binary form and do the call.
Design patterns are general reusable solutions applicable to recurring problems in software design. The meta-data and the reflective utilities provided by Mirror can be used for the concrete implementations of these design patterns.
One example of such utility is the factory generator, which allows to create factory classes that can be used to construct instances of a Product
type through a generic interface. The generated factories do not require the caller to supply the parameters for the construction directly. Such factories pick or let the application user pick the Product
's most appropriate constructor, they gather the necessary parameters in a generic, application-specific way and use the selected constructor to create an instance of the Product
. These factories can then be used to implement the abstract factory, [wikipedia.org] the factory method [wikipedia.org] or in the case of complex object constructions even the builder [wikipedia.org] design patterns.
Since reflection is especially handy when implementing persistence-related operations, it can be also used for the implementation of the memento [wikipedia.org] pattern, where the caretaker object can use reflection to create a snapshot of the internal state of another object and to restore this internal state in case of a rollback operation.