data::registry

data::registry is a lightweight, header-only C++(>=14) library that lets users obtain read-only or read-write access to data shared across translation units, or to data returned by user-defined functions.

The core idea is to define data entries identified by a tag, a data type, and, optionally, read and write accessors.

Step 1 - Declare entries

#include <data/registry.h>

// static variable as an entry
reg_e(speed, unsigned int);
reg_e(user, user_t);

// static entries must be stored in a traslation unit
reg_store_e(speed); 

// you can provide init value or the entire list of constructor arguments
reg_store_e(user, "John", "Doe", "john.doe@jd.com");

// readonly entry accessible via reader
reg_e(temperature, float, read_temperature_sensor);

// read-write entry accessible via reader and writer
reg_e(logs, std::string, read_logs, write_logs); 

Step 2 - Access entries via reg::get() and reg::set() by tag

// access function might have 0 to N number of parameters (contexts)
auto value = reg::get<tag>(ctx1, ..., ctxN);
reg::set<tag>(new_value, ctx1, ..., ctxN);

Features

  • No exceptions
    The data::registry API never throws exceptions internally; any exceptions can originate only from user-provided functions.

  • Context data support
    When needed, callers may pass context objects to reg::get() or reg::set(), provided the corresponding user-provided readers or writers accept those contexts as parameters.

  • Compatibility
    Supported and tested on C++14 through C++23.

  • No generators

  • No lookup tables
    Data is accessed at O(1).

Static entries

One way to use data::registry is to define entries as simple static variables by reg_e() macro with just two arguments - a tag and a type.

Example:

// .h
reg_e(speed, unsigned short);
// .cpp
/*
 * Here we store static variable associated with "speed"-tag in current
 * translation unit and (optionally) initialize with the value of 30.
 * If we don't specify the second argument, then the static value will
 * be default-constructed.
 */
reg_store_e(speed, 30);
// usage
void override_speed()
{
    std::cout << "initial speed is " << reg::get<speed>() << '\n';

    reg::set<speed>(60U);

    std::cout << "speed is set to " << reg::get<speed>() << '\n';
}

NOTE

For static entries, fundamental data types are returned by value, whereas more complex types are returned as const references. This policy avoids unnecessary copying when you call reg::get<...>().

Another way to use data::registry is through read and write accessors (readers & writers).

Readers & Writers

If the data is accessible only through custom functions or callable objects — for example, when it must be retrieved by a function that reads from hardware — you can still expose it to data::registry by defining appropriate reader and writer.

Example:

// .h
reg_e(speed, uint16_t, get_speed);
// usage
int foo()
{
    auto spd = reg::get<speed>();

    // ...
}

NOTE

We don’t need to store the data in any translation unit with the _store_e() macro, because the reader (get_speed()) defines the data and is responsible for retrieving it from its source.

Contexts

Sometimes, user needs to pass additional data while reading or writing the value of an entry. It can be a single variable, or the whole set of them. Let's say we need to read and write to the entry value in a thread-safe manner. In such case we can pass a synchronization primitive as a context to both reader and writer. The best way to do this is to pass std::shared_mutex (C++17), or boost::shared_mutex (C++14) as a context to both accessor functions.

Example:

// .h

// reads current temperature
float get_temperature(std::shared_mutex& mx);

// sets the temperature to desirable value
void set_temperature(float temp, std::shared_mutex& mx);
// .cpp - possible implementation of accessors
float get_temperature(std::shared_mutex& mx)
{
    std::shared_lock lk(mx);
    return read_temperature();
}

void set_temperature(float temp, std::shared_mutex& mx)
{
    std::unique_lock lk(mx);
    set_pref_temperature(temp);
}

reg_e(temperature, float, get_temperature, set_temperature);
// usage

static std::shared_mutex g_mx;

void manipulate_temperature()
{
    // ...

    std::cout << "temperature before " << reg::get<temperature>(g_mx) << '\n';

    reg::set<temperature>(20, g_mx);

    std::cout << "temperature after " << reg::get<temperature>(g_mx) << '\n';

    // ...
}

Keep in mind, that it's also possible to pass multiple contexts, in case if user-defined readers and (or) writers have corresponding function parameters.

Example:

// .h
enum class room_t : unsigned char {
    bathroom,
    bedroom,
    kitchen,
    livingroom
};

// let's say we have a reader function for a temperature for different
// appartments of a big appartment complex
float get_room_temperature(
    std::shared_mutex& mx,
    unsigned int appartment_no,
    room_t room);

reg_e(temperature, float, get_room_temperature);
// .cpp
float get_room_temperature(
    std::shared_mutex& mx,
    unsigned int appartment_no,
    room_t room)
{
    std::shared_lock lk(mx);
    return read_sensor_data(appartment_no, room);
}
// usage
static std::shared_mutex g_mx;

void check_all_kitchens()
{
    // iterating through all appartment numbers
    for (auto const apt_no : all_appartments)
    {
        if (reg::get<temperature>(g_mx, apt_no, room_t::kitchen) > 30.f)
        {
            notify_fire_dpt(apt_no);
        }
    }
}