Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Design Patterns: RunTime Reflection – C++

0.00/5 (No votes)
22 Apr 2023CPOL3 min read 4K  
RunTime Reflection in C++
One of the most desired Design Patterns is reflection - The ability to use classes metadata (names, functions, properties, etc.) at runtime. By default, this is not possible in C++, and sometimes the selected solution is to use macros, but today, we are going to achieve it without them.

More Design Patterns Articles

Call a Class Function by Its Name

You have the function’s name in a variable, and an instance of the class which this function belongs to:

C++
// ...
my_class mc;
std::string function_name = "my_func";
// ...

Now all you want to do is to connect the two into a calling, something like this:

C++
mc.(function_name)();

Unfortunately, this won’t work. The processor doesn’t like to let us fool around with the metadata of the classes, so we have to find another way to store this data for ourselves. Lucky us: It’s possible. Actually, we saw an implementation for this in the first article on this series: Hash your Conditions – C++.

C++
class my_class {
public:
    void function_a() const { std::cout << "my_class::function_a\n"; }
    void function_b() const { std::cout << "my_class::function_b\n"; }
    void function_c() const { std::cout << "my_class::function_c\n"; }
};

class my_class_functions_collection {
private:
    using my_class_func_t = void(my_class::*)() const;

public:
    explicit my_class_functions_collection(my_class *my_class_ptr) : 
                                           mc_ptr(my_class_ptr) {
        functions_collection = {
                {"function_a", &my_class::function_a},
                {"function_b", &my_class::function_b},
                {"function_c", &my_class::function_c},
        };
    }

    void call_function(std::string &&func_name) {
        (mc_ptr->*(functions_collection.at(func_name)))();
    }

private:
    std::map<std::string, my_class_func_t> functions_collection;
    my_class *mc_ptr;
};

int main() {
    my_class mc;
    my_class_functions_collection mc_functions(&mc);

    std::string desired_function;
    std::cin >> desired_function;
    mc_functions.call_function("function_a");
    mc_functions.call_function("function_b");
    mc_functions.call_function(std::move(desired_function));

    return EXIT_SUCCESS;
}

Pay attention that all functions have to accept the same params and return the same value, and yet functions metadata usage – ACHIEVED!

Class Properties

Using the same idea of class member functions reflection, we can also achieve class member variables reflection - assuming they all have the same type:

C++
class my_class { // Can be made struct as well
public:
    int a;
    int b;
    int c;
    int d;
};

class my_class_available_vars {
private:
    using my_class_int_variable = int(my_class::*);
    
public:
    my_class_available_vars() {
        available_vars_collection = {
                {"a", &my_class::a},
                {"b", &my_class::b},
                {"c", &my_class::c},
                {"d", &my_class::d},
        };
    }

    [[nodiscard]] my_class_int_variable get_var_md(const std::string &var_name) const {
        return available_vars_collection.at(var_name);
    }

private:
    std::map<std::string, my_class_int_variable> available_vars_collection;
};

int main() {
    my_class mc;
    mc.a = 3;
    mc.b = 56;

    my_class_available_vars mc_vars;
    std::cout << (mc.*(mc_vars.get_var_md("a"))) << "\n"; // 3
    std::cout << (mc.*(mc_vars.get_var_md("b"))) << "\n"; // 56
    (mc.*(mc_vars.get_var_md("b"))) = 17;
    std::cout << (mc.*(mc_vars.get_var_md("b"))) << "\n"; // 17

    std::string variable;
    int val;
    std::cin >> variable >> val;

    (mc.*(mc_vars.get_var_md(variable))) = val; // 17
    std::cout << "mc." << variable << " = " << 
                 (mc.*(mc_vars.get_var_md(variable))) << "\n"; // 17

    return EXIT_SUCCESS;
}

But usually, it’s not the case. We have to find another way to enable choosing class variables of different types, let’s spice this my_class_available_vars a little bit with std::variant.

Our structure now contains variables with different types:

C++
class my_class {
public:
    int i;
    float f;
    std::string s;
    double d;
};

The variant in my_class_available_vars should enable these types of my_class. Something like that:

C++
std::variant<int(my_class::*), float(my_class::*), 
std::string(my_class::*), double(my_class::*)>

And in order to make this variant stand still at the scale moment, let’s separate it to multiple parts:

C++
using desired_class = my_class;
using desired_class_int = int(desired_class::*);
using desired_class_double = double(desired_class::*);
using desired_class_float = float(desired_class::*);
using desired_class_string = std::string(desired_class::*);
using desired_class_variant = std::variant<desired_class_int, 
desired_class_float, desired_class_string, desired_class_double>;

Now that we’ve got the right variant, it’s time to prepare our my_class_available_vars class:

C++
class my_class_available_vars {
public:
    using desired_class = my_class;
    using desired_class_int = int(desired_class::*);
    using desired_class_double = double(desired_class::*);
    using desired_class_float = float(desired_class::*);
    using desired_class_string = std::string(desired_class::*);
    using desired_class_variant = std::variant<desired_class_int, 
    desired_class_float, desired_class_string, desired_class_double>;
    using simple_types_variant = std::variant<int, float, std::string, double>;

public:
    my_class_available_vars() {
        available_vars_collection = {
                {"i", &desired_class::i},
                {"f", &desired_class::f},
                {"s", &desired_class::s},
                {"d", &desired_class::d},
        };
    }

    [[nodiscard]] desired_class_variant get_var_md(const std::string &var_name) const {
        return available_vars_collection.at(var_name);
    }

private:
    std::map<std::string, desired_class_variant> available_vars_collection;
};

Now it’s time to use this monster inside our code:

C++
// helper type for the visitor
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    my_class mc;
    mc.i = 3;
    mc.s = "My str";

    my_class_available_vars mc_vars;
    my_class_available_vars::desired_class_variant desired_variable; // Will contain 
                                           // one of the available my_class variables.

    // Print function for desired_variable
    auto print_var = [&mc] 
        (my_class_available_vars::desired_class_variant desired_variable) {
        std::visit(overloaded {
                [&](auto variable) { std::cout << mc.*(variable) << "\n"; },
        }, desired_variable);
    };

    print_var(mc_vars.get_var_md("i")); // 3
    print_var(mc_vars.get_var_md("s")); // My str

    // ...
}

Now let’s take this one step ahead and enable to set a new value for our class variables. In order to do so in a single function, we need to accept three variables:

  • A class instance (my_class instance)
  • A desired class variable (desired_variable)
  • A new value which can be any type of the class, but without class associate

For the third requirement, we have to create a new variant inside my_class_available_vars, which will contains the same types without my_class associate:

C++
using simple_types_variant = std::variant<int, float, std::string, double>;

For the simplicity of this example, I made these variant types with the exact same order of desired_class_variant, so we’ll be able to easily detect if the new_value type matches the desired variable type. The con here is the difficulty in maintenance, but you are welcome to offer another check.

C++
// A function to set new value to a class variable
auto set_var = [&mc] (my_class_available_vars::desired_class_variant desired_variable, 
                      const my_class_available_vars::simple_types_variant &new_val) {
        if (desired_variable.index() != new_val.index()) 
        throw std::runtime_error
        ("Desired variable doesn't match new_val type."); // Check for types matching
        std::visit(overloaded {
            [&](my_class_available_vars::desired_class_int variable) 
            { mc.*(variable) = std::get<int>(new_val); },
            [&](my_class_available_vars::desired_class_float variable) 
            { mc.*(variable) = std::get<float>(new_val); },
            [&](my_class_available_vars::desired_class_string variable) 
            { mc.*(variable) = std::get<std::string>(new_val); },
            [&](my_class_available_vars::desired_class_double variable) 
            { mc.*(variable) = std::get<double>(new_val); },
        }, desired_variable);
};

Now that we’ve got a setter function, it’s time to use it:

C++
int main() {
    // ...

    desired_variable = mc_vars.get_var_md("s");
    set_var(desired_variable, "My new string");
    print_var(desired_variable); // My new string

    desired_variable = mc_vars.get_var_md("d");
    set_var(desired_variable, 0.56);
    print_var(desired_variable); // 0.56

    // ...
}

To take it a little bit further, I decided to add a section of input from the user:

C++
void input_value_based_on_variable_type
(my_class_available_vars::simple_types_variant& user_new_val, 
 const std::string &variable) {
    // The following section can be improved...
    if (variable == "i") {
        int i;
        std::cout << "Enter integer:\n";
        std::cin >> i;
        user_new_val = i;
    } else if (variable == "f") {
        float f;
        std::cout << "Enter float:\n";
        std::cin >> f;
        user_new_val = f;
    } else if (variable == "s") {
        std::string s;
        std::cout << "Enter string:\n";
        std::getline(std::cin >> std::ws, s);
        user_new_val = s;
    } else if (variable == "d") {
        double d;
        std::cout << "Enter double:\n";
        std::cin >> d;
        user_new_val = d;
    } else {
        throw std::runtime_error("Variable not found.");
    }
}

int main() {
    // ...

    std::string variable;
    std::cout << "Choose variable (i, f, s, d):\n";
    std::cin >> variable;

    my_class_available_vars::simple_types_variant user_new_val;
    input_value_based_on_variable_type(user_new_val, variable);

    desired_variable = mc_vars.get_var_md(variable);
    set_var(desired_variable, user_new_val);
    print_var(desired_variable);

    return EXIT_SUCCESS;
}

Class properties reflection – ACHIEVED!

Class Names

Using the same design of class properties reflection, we can create classes_collector class which will generate for us the relevant class by its name:

C++
struct struct_a {
    int i;
};

struct struct_b {
    std::string str;
    double d;
};

struct struct_c {};
struct struct_d {};

class classes_collector {
public:
    using desired_class_variant = std::variant<struct_a, struct_b, struct_c, struct_d>;

public:
    classes_collector() {
        available_vars_collection = {
                {"struct_a", struct_a()},
                {"struct_b", struct_b()},
                {"struct_c", struct_c()},
                {"struct_d", struct_d()},
        };
    }

    [[nodiscard]] desired_class_variant get_class_by_name
                  (const std::string &var_name) const {
        return available_vars_collection.at(var_name);
    }

private:
    std::map<std::string, desired_class_variant> available_vars_collection;
};

Now we can use it in our function:

C++
// helper type for the visitor
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    classes_collector classes;
    classes_collector::desired_class_variant desired_class;

    desired_class = classes.get_class_by_name("struct_a");
    std::get<struct_a>(desired_class).i = 19;
    std::visit(overloaded {
        [](struct_a &s_a) { std::cout << s_a.i << "\n"; },
        [](struct_b &s_b) { std::cout << s_b.str << " " << s_b.d << "\n"; },
        [](auto another) {},
    }, desired_class);

    // ...
}

A more complex example, we can see based on my personal experience with my team:
Our system generated a message and passed it between threads using cpp-ipc. The problem was that the data we needed to pass was a complex data so we had to use heap allocation to pass it safely. Due to the ability of each thread to accept multiple types of data, we had to build a data parser, based on a message metadata which was an enum, symbolic to class type. And here we are: Building a class type parser:

C++
int main() {
    // ...

    struct available_data {
        std::string struct_type;
        void* struct_data;
    };

    available_data data;
    data.struct_type = "struct_b";
    data.struct_data = new struct_b {
        .str = "Find Me!",
        .d = 1465.165
    };

    desired_class = classes.get_class_by_name(data.struct_type);
    std::visit(overloaded {
            [&](struct_a &s_a) {
                auto data_parser = static_cast<struct_a*>(data.struct_data);
                std::cout << data_parser->i << "\n";
                delete data_parser;
            },
            [&](struct_b &s_b) {
                auto data_parser = static_cast<struct_b*>(data.struct_data);
                std::cout << data_parser->str << " " << data_parser->d << "\n";
                delete data_parser;
            },
            [&](struct_c &s_c) {
                auto data_parser = static_cast<struct_c*>(data.struct_data);
                delete data_parser;
            },
            [&](struct_d &s_d) {
                auto data_parser = static_cast<struct_d*>(data.struct_data);
                delete data_parser;
            },
    }, desired_class);

    return EXIT_SUCCESS;
}

The big con here is that we generate a new class only for deducing its type. To solve this issue, we can create a map with std::type_index value type:

C++
class class_types_collector {
public:
    class_types_collector() {
        available_vars_collection = {
                {"struct_a", typeid(struct_a)},
                {"struct_b", typeid(struct_b)},
                {"struct_c", typeid(struct_c)},
                {"struct_d", typeid(struct_d)},
        };
    }

    [[nodiscard]] std::type_index get_class_by_name(const std::string &var_name) const {
        return available_vars_collection.at(var_name);
    }

private:
    std::map<std::string, std::type_index> available_vars_collection;
};

And for our main:

C++
int main() {
    // ...

    class_types_collector class_types;

    struct available_data {
        std::string struct_type;
        void* struct_data;
    };

    available_data data;
    data.struct_type = "struct_b";
    data.struct_data = new struct_b{
            .str = "Find Me!",
            .d = 1465.165
    };

    auto desired_class_type = class_types.get_class_by_name(data.struct_type);
    if (desired_class_type == typeid(struct_a)) {
        auto data_parser = static_cast<struct_a *>(data.struct_data);
        std::cout << data_parser->i << "\n";
        delete data_parser;
    } else if (desired_class_type == typeid(struct_b)) {
        auto data_parser = static_cast<struct_b *>(data.struct_data);
        std::cout << data_parser->str << " " << data_parser->d << "\n";
        delete data_parser;
    } else if (desired_class_type == typeid(struct_c)) {
        auto data_parser = static_cast<struct_c *>(data.struct_data);
        delete data_parser;
    } else {
        auto data_parser = static_cast<struct_d *>(data.struct_data);
        delete data_parser;
    }

    return EXIT_SUCCESS;
}

Class name reflection – ACHIEVED!

Conclusion

The only impossible is the impossible. I hope you enjoyed your reading, and that this article helps you to see things differently than before or even to solve some issues in your code. If you have advice for making things better, please share them in the comments.

Checkout the GitHub repository with examples: cppsenioreas-design-pattern-reflection.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)