This is just a summarizing readme providing the most vital information. The official documentation can be found at https://cppproperties.insane.engineer.
This is a C++20 library providing a property system to client classes.
The library is built with the following aspects in mind:
- Modern C++
- Easy to use
- Providing "raw access" to the properties just as if they were regular class members.
- Easy registration of custom property types.
- Easy integration of optional (de)serialization (XML & JSON already optionally built-in).
- Observer interface for property change notifications.
- Support for linked properties (properties in a base class not implementing this library).
- GUI generator (Qt widgets)
A couple of things to be aware of when using this library:
- Requires a C++20 capable compiler
- Properties are stored on the heap
- The memory layout of struct { MAKE_PROPERTY(a, int) };is not the same asstruct { int a; };
- Property change notification observer callbacks are invoked by which ever thread modified the property value.
This library is MIT licensed.
- If JSON (de)serialization is enabled, nlohmann::json is used for JSON serialization. The json library itself is MIT licensed.
- If XML (de)serialization is enabled, tinyxml2 is used for XML serialization. The tinyxml2 library itself is zlib licensed.
Any type can be registered as a property type using the REGISTER_PROPERTY macro.
For convenience, a set of built-in types are already registered:
- bool
- int
- float
- double
- std::basic_string<T>(eg.- std::string,- std::wstring, ...)
- std::filesystem::path
If the cmake option CPPPROPERTIES_ENABLE_BOOST is set to ON, the following types are also built-in:
- boost:uuids::uuid
If the cmake option CPPPROPERTIES_ENABLE_QT is set to ON, the following types are also built-in:
- QString
- QPoint
Start by reading the Usage section below. More examples can be found in the examples directory.
Basic usage only requires inheriting from tct::properties::properties and adding properties using MAKE_PROPERTY():
struct shape :
    tct::properties::properties
{
    MAKE_PROPERTY(x, float);
    MAKE_PROPERTY(y, float);
};The defined properties may now be used as if they were just class members of type float:
int main(void)
{
    shape s;
    s.x = 24.48f;
    s.y = -13.29f;
  
    // Print them 
    std::cout << "s.x = " << s.x << std::endl;
    std::cout << "s.y = " << s.y << std::endl;
}Custom types may be used after registering them with REGISTER_PROPERTY():
/**
 * Define a custom type 'color'.
 */
struct color
{
    std::string name;
    uint8_t r, g, b;
    [[nodiscard]] std::string to_string() const
    {
        // ...
        return { };
    }
    void from_string(const std::string& str)
    { 
        // ...
    }
};
/**
 * Register the property
 */
REGISTER_PROPERTY(
    color,
    [this](){ return data.to_string(); },
    [this](const std::string& str){ this->data.from_string(str); }
)
/**
 * Client class using properties.
 */
struct shape :
    tct::properties::properties
{
    MAKE_PROPERTY(x, float);
    MAKE_PROPERTY(y, float);
    MAKE_PROPERTY(stroke_color color);
    MAKE_PROPERTY(fill_color color);
};Properties allow registering observers to notify them upon changes of the property value.
struct shape :
    tct::properties::properties
{
    MAKE_PROPERTY(x, float);
    MAKE_PROPERTY(y, float);
    shape()
    {
        x.register_observer([](){ std::cout << "x property changed!\n"; });
        y.register_observer([](){ std::cout << "y property changed!\n"; });
    }
};
int main()
{
    shape s;
    s.x = 42;   // Prints "x property changed!";
    s.y = 73;   // Prints "y property changed!";
    return 0;
}The library comes with built-in support for (de)serialization. Classes can be easily (de)serialization to/from XML:
struct shape :
    tct::properties::properties
{
    MAKE_PROPERTY(x, float);
    MAKE_PROPERTY(y, float);
}
int main(void)
{
    // Create a shape
    shape s;
    s.x = 13;
    s.y = 37;
 
    // Serialize to std::string using XML format   
    const std::string& xml_string =  s.to_xml();
    std::cout << xml_string << std::endl;
    
    // Serialize to XML file
    s.to_xml_file("/path/to/shape.xml");
    // Deserialize from std::string
    shape s2;
    s2.from_xml(xml_string);
    // Deserialize from XML file
    shape s3;
    s3.from_xml_file("/path/to/shape.xml");
    return 0;
}One is likely to encounter a scenario where a client class derived inherits from tct::properties::properties but also from another, existing base class base.
In this case serializing an instance of derived will only contain the properties created with MAKE_PROPERTY. However, one might like (or need) to also include members of the base class although these members are not registered as properties in the base class.
An example:
struct base
{
    int x;
    int y;
};
struct derived :
    public base,
    public tct::properties::properties
{
    MAKE_PROPERTY(name, std::string);
};Serializing instances of type derived will contain the name properties but not other vital information such as X & Y coordinates which are public members of base. In this cae, LINK_PROPERTY() may be used to include them in (de)serialization too:
struct base :
{
    int x;
    int y;
};
struct derived :
    public base,
    public tct::properties::properties
{
    MAKE_PROPERTY(name, std::string);
    LINK_PROPERTY(x, &x);
    LINK_PROPERTY(y, &y);
};This is similar to Linked properties but instead of directly accessing a base class member we use the corresponding getter & setters. This way, members from a base class only accessible via getters & setters can be included in (de)serialization.
An example:
struct base
{
public:
    void set_x(const int x) { m_x = x; }
    [[nodiscard]] int x() const { return m_x; }
private:
    int m_x = 0;
};
struct derived :
    base,
    tct::properties::properties
{
    derived()
    {
        LINK_PROPERTY_FUNCTIONS(x, int, base::set_x, base::x)
    }
};If CPPPROPERTIES_ENABLE_QT_WIDGETS is set to ON, Qt based widgets can be generated automatically for a property or a property group:
#include <iostream>
#include <QApplication>
#include <QWidget>
#include "cppproperties/properties.hpp"
#include "cppproperties/qt_widgets/factory.hpp"
struct shape :
    tct::properties::properties
{
    MAKE_PROPERTY(enabled, bool);
    MAKE_PROPERTY(x, int);
    MAKE_PROPERTY(y, int);
    shape()
    {
        enabled.register_observer([](){ std::cout << "enabled changed!\n"; });
        x.register_observer([](){ std::cout << "x changed!\n"; });
        y.register_observer([](){ std::cout << "x changed!\n"; });
    }
};
struct circle :
    shape
{
    MAKE_PROPERTY(radius, int);
    circle()
    {
        radius.register_observer([](){ std::cout << "radius channged!\n"; });
    }
};
int main(int argc, char* argv[])
{
    QApplication a(argc, argv);
    circle s;
    // Set some property values
    s.x = 24;
    s.y = 48;
    s.radius = 14;
    // Create widget
    auto w = tct::properties::qt_widgets::factory::build_form(s);
    if (w)
        w->show();
    return a.exec();
}This library provides a doctest based test suite under /test. The corresponding cmake target is tests.