Два типа рефлексий в C++

92cabee6dcbc9ae5898af66616329c02.png

Привет, Хабр!

Задумывались ли вы когда-нибудь, что вашему коду стоило бы пройти сеанс психотерапии? В C++ это возможно благодаря такой замечательной штуке, как рефлексия. Она позволяет вашему коду буквально видить в зеркале себя и понимать свои ошибки и достоинства.

Итак, рефлексия — это процесс, при котором программа может инспектировать и изменять структуру и поведение во время выполнения.

Рефлексия в C++ бывает двух основных типов: компиляционная и рефлексия времени выполнения. Оба типа имеют свои особенности и применяются в различных сценариях.

Компиляционная рефлексия

Компиляционная рефлексия, также известная как статическая рефлексия, позволяет получать информацию о типах и их членах во время компиляции. Основной подход к реализации компиляционной рефлексии в C++ основан на использовании шаблонного метапрограммирования и constexpr функций.

Рассмотрим пример с библиотекой refl-cpp, специально созданной для компиляционных рефлексий.

Добавляем заголовочный файл refl.hpp в проект. Библиотека с одним заголовочным файлом, поэтому интеграция очень проста:

#include "refl.hpp"

Определим простую структуру Point с двумя полями типа float:

struct Point {
    float x;
    float y;
};

// активируем рефлексию для структуры Point
REFL_TYPE(Point)
REFL_FIELD(x)
REFL_FIELD(y)

Теперь можно использовать refl-cpp для получения метаданных о типе Point и его членах:

int main() {
    Point pt = {1.0f, 2.0f};
    auto type = refl::reflect(pt);

    refl::util::for_each(type.members, [&](auto member) {
        std::cout << "Member name: " << member.name << std::endl;
        std::cout << "Member value: " << member(pt) << std::endl;
    });

    return 0;
}

В этом примере используем refl::reflect для получения дескриптора типа Point. Затем с помощью refl::util::for_each и лямбда-функции перебираем все члены структуры, выводя их имена и значения.

С помощью refl-cpp можно также реализовать функции сериализации и десериализации:

#include "refl.hpp"
#include 

struct Serializable {
    float a;
    int b;

    REFL_TYPE(Serializable)
    REFL_FIELD(a)
    REFL_FIELD(b)
};

template 
std::string serialize(const T& obj) {
    std::ostringstream oss;
    auto type = refl::reflect(obj);

    refl::util::for_each(type.members, [&](auto member) {
        oss << member.name << ":" << member(obj) << ";";
    });

    return oss.str();
}

template 
T deserialize(const std::string& data) {
    std::istringstream iss(data);
    T obj;
    auto type = refl::reflect(obj);
    std::string token;

    while (std::getline(iss, token, ';')) {
        auto pos = token.find(':');
        if (pos != std::string::npos) {
            std::string name = token.substr(0, pos);
            std::string value = token.substr(pos + 1);

            refl::util::for_each(type.members, [&](auto member) {
                if (member.name == name) {
                    std::istringstream(value) >> member(obj);
                }
            });
        }
    }

    return obj;
}

int main() {
    Serializable s1 = {3.14f, 42};
    std::string serialized = serialize(s1);
    std::cout << "Serialized: " << serialized << std::endl;

    Serializable s2 = deserialize(serialized);
    std::cout << "Deserialized: a=" << s2.a << ", b=" << s2.b << std::endl;

    return 0;
}

Сначала сериализуем объект Serializable в строку, а затем десериализуем эту строку обратно в объект.

refl-cpp также поддерживает шаблонные типы и перегруженные функции:

template 
struct Container {
    T value;

    void print() const {
        std::cout << "Value: " << value << std::endl;
    }

    REFL_TYPE(Container)
    REFL_FIELD(value)
    REFL_FUNC(print)
};

int main() {
    Container c = {123};
    auto type = refl::reflect(c);

    refl::util::for_each(type.members, [&](auto member) {
        std::cout << "Member name: " << member.name << std::endl;
        std::cout << "Member value: " << member(c) << std::endl;
    });

    return 0;
}

Так можно использовать рефлексию для работы с шаблонными типами и вызова функций-членов.

Рефлексия времени выполнения (RTTI)

RTTI позволяет получать информацию о типах и их членах во время выполнения программы. Используется для реализации полиморфизма и позвояет определять тип объекта во время выполнения.

Будем юзать dynamic_cast для безопасного приведения указателя базового класса к указателю производного класса:

#include 
#include 

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void show() {
        std::cout << "Derived class method called." << std::endl;
    }
};

int main() {
    Base* base_ptr = new Derived();
    Derived* derived_ptr = dynamic_cast(base_ptr);

    if (derived_ptr) {
        std::cout << "Successfully casted to Derived." << std::endl;
        derived_ptr->show();
    } else {
        std::cout << "Failed to cast to Derived." << std::endl;
    }

    delete base_ptr;
    return 0;
}

Код проверяет тип объекта во время выполнения и безопасно выполняет приведение типа с помощью dynamic_cast.

Теперь будем использовать RTTI для создания полиморфной фабрики, которая создает объекты разных типов на основе их имен типов:

#include 
#include 
#include 
#include 
#include 
#include 

class Base {
public:
    virtual ~Base() {}
    virtual std::unique_ptr clone() const = 0;
};

class DerivedA : public Base {
public:
    std::unique_ptr clone() const override {
        return std::make_unique(*this);
    }
};

class DerivedB : public Base {
public:
    std::unique_ptr clone() const override {
        return std::make_unique(*this);
    }
};

class Factory {
public:
    template 
    void registerType() {
        std::string typeName = typeid(T).name();
        creators[typeName] = []() -> std::unique_ptr {
            return std::make_unique();
        };
    }

    std::unique_ptr create(const std::string& typeName) {
        auto it = creators.find(typeName);
        if (it != creators.end()) {
            return it->second();
        }
        return nullptr;
    }

private:
    std::map()>> creators;
};

int main() {
    Factory factory;
    factory.registerType();
    factory.registerType();

    std::unique_ptr objA = factory.create(typeid(DerivedA).name());
    std::unique_ptr objB = factory.create(typeid(DerivedB).name());

    if (objA) {
        std::cout << "Created object of type: " << typeid(*objA).name() << std::endl;
    }

    if (objB) {
        std::cout << "Created object of type: " << typeid(*objB).name() << std::endl;
    }

    return 0;
}

Так можно юзать RTTI для создания объектов различных типов динамически в зависимости от их имен типов.

Еще есть библиотека, которая RTTR позволяет регистрировать и манипулировать свойствами и методами классов во время выполнения.

Пример использования RTTR для регистрации и доступа к свойствам и методам:

#include 
#include 
#include 

using namespace rttr;

struct MyStruct {
    MyStruct() : data(0) {}
    void func(double val) { std::cout << "Function called with value: " << val << std::endl; }
    int data;
};

RTTR_REGISTRATION {
    registration::class_("MyStruct")
        .constructor<>()
        .property("data", &MyStruct::data)
        .method("func", &MyStruct::func);
}

int main() {
    type t = type::get();
    MyStruct obj;

    // установка значения свойства
    property prop = t.get_property("data");
    prop.set_value(obj, 42);

    // получение значения свойства
    variant var = prop.get_value(obj);
    std::cout << "Value of 'data': " << var.to_int() << std::endl;

    // вызов метода
    method meth = t.get_method("func");
    meth.invoke(obj, 3.14);

    return 0;
}

Так можно юзать RTTR для регистрации класса, установки и получения значений свойств и вызова методов во время выполнения.

В итоге: какой тип рефлексии выбрать?

Компиляционная рефлексия:

Если требуется максимально эффективный код без накладных расходов во время выполнения, компиляционная рефлексия — лучший выбор. Она выполняет все операции на этапе компиляции, что исключает дополнительные затраты времени при выполнении.

Если важна строгая проверка типов во время компиляции для предотвращения ошибок времени выполнения.

При необходимости создания сложных обобщённых алгоритмов и структур данных, которые должны быть безопасными и оптимизированными.

Для реализации механизмов сериализации данных с высокой производительностью и безопасностью.

Рефлексия времени выполнения (RTTI):

При необходимости работы с иерархиями классов и объектов, которые могут изменяться во время выполнения, и требуется безопасное приведение типов.

В системах, поддерживающих динамическую загрузку плагинов, для проверки совместимости типов.

Когда нужно работать с типами, которые известны только во время выполнения, например, в системах сериализации и десериализации.

В завершение хочу порекомендовать вам бесплатные вебинары специализации C++:

© Habrahabr.ru