New major release of RTTR (0.9.6)

Axel Menzel Comments releases

Finally, after almost 2 years, and over 400 commits, I increased the version number of RTTR to version 0.9.6 Besides really interesting bug fixes, this blog post, will show the newest features and changes.

Retrieve item's via views

The suggestion came from Sean Middleditch. Instead of returning a std::vector by value for all properties (and allocating memory) I should return a view to the underlying data.

Therefore I implemented the array_range class. See following example code:

#include <rttr/registration>
using namespace rttr;

struct player { void boost(int value) { return true; } };

RTTR_REGISTRATION { rttr::registration::class_<player>("player").method("boost", &player::boost); }
int main(){
    rttr::array_range<method> meth_range = type::get<player>().get_methods()
    for (auto meth : meth_range)
        std::cout << meth.get_signature() << std::endl;
}

Output:

boost( int )

Additionally, using this approach it was quite easy to add the some filter condition to the range. For example, filter only for private methods: get_methods(filter_item::instance_item | filter_item::non_public_access) A full example is here.

Furthermore, a string_view class was added. Every previous method returning a std::string, was replaced with returning a rttr::string_view. See for example type::get_name(). Side note, because RTTR still works with c++11 compilers, I had to implement this class myself and not used the new C++17 std::string_view.

New associative- and sequential-viewer classes

In order to add generic access to associative and sequential container classes, two new variant viewer classes were added:

  1. variant_associative_view
  2. variant_sequential_view

See following example code for the usage of variant_associative_view:

std::map<int, std::string> my_map = { { 1, "one" }, { 2, "two" }, { 3, "three" } };
variant var = my_map;
if (var.is_associative_container())
{
    variant_associative_view view = var.create_associative_view();
    std::cout << view.get_size() << std::endl;      // prints: '3'
    for (const auto& item : view)
    {
        // remark that the key and value are stored inside a 'std::reference_wrapper'
        std::cout << "Key: " << item.first.extract_wrapped_value().to_string() << " ";
        std::cout << "Value: " << item.second.extract_wrapped_value().to_string() << std::endl;
    }
}

It's of course possible to nest the containers. because when iterating over the items, variant objects with a std::reference_wrapper<T> are returned. This allows very easily to write serialization code for complex containers, independent of the underlying data structure. The variant_array_view class was removed, all functionality is available through variant_sequential_view.

New features for variant class

A lot of nice changes were added to the variant class.

  1. New method to extract a wrapper value inside a variant: get_wrapped_value()

    int value = 23;
    variant var = std::ref(value);
    if (var.get_type().get_wrapped_type() == type::get<int>())  // yields to true
        const int& ref_value = var.get_wrapped_value<int>();    // extracts the value by reference
  2. Copying the wrapped value inside a variant, into another variant: extract_wrapped_value()

    int value1 = 23;
    variant var1 = std::ref(value1);
    if (var1.get_type().get_wrapped_type() == type::get<int>())  // yields to true
    {
        variant var2 = var1.extract_wrapped_value(); // value will be copied into "var2"
        var2.get_type() == type::get<int>(); // yields to true
        const int& value2 = var2.get_value<int>();
        std::cout << value2 << std::endl;    // prints "23"
    }
  3. Implicit conversion from wrapped value to concrete type:

    int value = 23;
    variant var = std::ref(value);
    // instead of writing this:
    std::cout << var.extract_wrapped_value().to_int() << std::endl;
    // user can now write this:
    std::cout << var.to_int() << std::endl;
  4. Added new comparators (>, <=, >=)
  5. Added 4 variant_cast functions, similar to std::any:

    variant var = std::string("hello world");
    std::string& a = variant_cast<std::string&>(var);
    int* a_ptr = variant_cast<int*>(&var);      // internal type check => nullptr returned
    std::string b = variant_cast<std::string>(std::move(var));  // move the value to 'b'
    std::cout << "a: " << a << std::endl;   // is now empty (nothing to print)
    std::cout << "b: " << b << std::endl;   // prints "hello world"
  6. Allow conversion from raw pointers, which are null, to base pointers
    struct base { RTTR_ENABLE() }; struct derived : base { RTTR_ENABLE(base) };
    derived* d = nullptr;
    var = d;
    var.convert(type::get<base*>()); // yields to `true`

New methods in the type class

  1. A method to check if the given type is the base class of another type: is_base_of(const type& t)

    type t = type::get<base>();
    t.is_base_of(type::get<derived>());    // yields to `true`
  2. Retrieve template arguments of a class template: array_range<type> get_template_arguments()
    auto type_range = type::get<vector<int>>().get_template_arguments();
    for (auto t : type_range)
        std::cout << t.get_name() << std::endl;

    Leads to following output (MSVC-compiler):

    int
    classstd::allocator<int>

    See also is_template_instantiation()

Loading of plugins

Now you can easily load and unload a library at runtime and the types are automatically exposed to the type system. I had to restructure some of the internal parts of RTTR to make this feature possible. Furthermore I added a class called library to wrap the nativ OS calls in order to load/unload a library. You also don't have to specify the native library prefix and suffix, quite handy when you want to write cross plattform code. See following example:

#include <iostream>

// sphere.cpp, is included in an external library called "my_plugin.dll"
struct sphere {
    double pos_x;
    double pos_y;
    double pos_z;

    void set_radius(int value) {
        if (value > 0)
            m_radius = value;
        else
            std::cout << "invalid radius: " << value << std::endl;
    }
    int get_radius() const { return m_radius; }

    void set_position(double x, double y, double z) { pos_x = x; pos_y = y; pos_z = z; }
private:
    int    m_radius;
};

#include <rttr/registration>

RTTR_PLUGIN_REGISTRATION
{
    rttr::registration::class_<sphere>("sphere")
        .constructor<>()
        .property("pos_x", &sphere::pos_x)
        .property("pos_y", &sphere::pos_y)
        .property("pos_z", &sphere::pos_z)
        .property("radius", &sphere::get_radius, &sphere::set_radius)
        .method("set_position", &sphere::set_position)
        ;
}

New is the macro RTTR_PLUGIN_REGISTRATION. It was necessary, in order to load the types when the library is loaded and automatically unregister the types, when the library is unloaded. So when you export your types in a plugin, use the new macro: RTTR_PLUGIN_REGISTRATION.

Somehwere in your main application:

#include <rttr/type>
using namespace rttr;
int main()
{
    library lib("my_plugin");
    lib.load();
    type t = type::get_by_name("sphere");
    property prop = t.get_property("radius");

    variant var = t.create();
    prop.set_value(var, 12);

    prop.set_value(var, 0); // prints "invalid radius: 0"
}

Like you can see there is not header file from the sphere class included. The class will be retrieved with its name via type::get_by_name. So it is easily possible to load and interact with types, where you don't have the type definition or that are even marked as exported in the library. The sphere class is completely hidden inside the library. The library class has further methods to retrieve the loaded types, methods or properties of the library at runtime. When using RTTR and its interface, you can even retrieve overloaded methods, which is not possible with standard C export.

For more information look at the small tutorial here or in the examples folder in RTTR repository.

Serialization with JSON

This is basically just an example of using reflection with RTTR. Serializing of an object is not a core part of RTTR.

I used rapid json as underlying json serializer API. Take a look at following example code:

We are still using our sphere class from the plugin.

type t = type::get_by_name("sphere");
variant var = t.create();
t.invoke("set_position", var, { 1.0, 2.5, 3.0 });
t.set_property_value("radius", var, 12);
std::string json_string = io::to_json(var);
std::cout << json_string << std::endl;

The function io::to_json is doing the serialization. You can look in the implementation here

Here is the output:

{
    "pos_x": 1.0,
    "pos_y": 2.5,
    "pos_z": 3.0,
    "radius": 12
}

What's really nice is, is that the serialization code is completely independent of the underlying type. You write the serialization code one time and that's it. Furthermore, you can specify via metadata, which properties should be serialized and which not. A pretty important feature, which you can not automatize via generators, you have to specify it manually.

Some side note: When you combine the serialization code with the plugin loading, you could easily implement hot reloading of plugins.

You can find the full blown example (serializing derived objects, having vectors and maps as properties, etc...) in the repository here.

Roadmap

On my next agenda is the possibility to bind RTTR to a scripting language. I did already some research of the various scripting languages available and how to implement this in a generic and easy to use way.

Closing words

A lot of my spare time was going into this new release. I hope you find it as useful as I do. When you have any questions or inquiries, you can drop me a line at info@rttr.org or contact me via the website.

You find the source code and binaries of this release in the download section or also on GitHub.


Previous Post

Blog Comments powered by Disqus.