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

Задумывались ли вы когда-нибудь, что вашему коду стоило бы пройти сеанс психотерапии? В 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 <sstream>

struct Serializable {
    float a;
    int b;

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

template <typename T>
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 <typename T>
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<Serializable>(serialized);
    std::cout << "Deserialized: a=" << s2.a << ", b=" << s2.b << std::endl;

    return 0;
}

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

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

template <typename T>
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<int> 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 <iostream>
#include <typeinfo>

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<Derived*>(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 <iostream>
#include <map>
#include <memory>
#include <string>
#include <functional>
#include <typeinfo>

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

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

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

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

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

private:
    std::map<std::string, std::function<std::unique_ptr<Base>()>> creators;
};

int main() {
    Factory factory;
    factory.registerType<DerivedA>();
    factory.registerType<DerivedB>();

    std::unique_ptr<Base> objA = factory.create(typeid(DerivedA).name());
    std::unique_ptr<Base> 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 <rttr/registration>
#include <iostream>
#include <string>

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>("MyStruct")
        .constructor<>()
        .property("data", &MyStruct::data)
        .method("func", &MyStruct::func);
}

int main() {
    type t = type::get<MyStruct>();
    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++:

Комментарии (11)


  1. sshmakov
    03.06.2024 08:15
    +9

    Обозвать RTTI рефлексией это мощно, молодёжно, современно.

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

    Читать так: Есть сторонняя библиотека RTTR (https://github.com/rttrorg/rttr), которая регистрирует свойства, методы и классы, после чего ими можно манипулировать. Что не было зарегистрировано, тем манипулировать нельзя. Что в целом не похоже на рефлексию в той же Java.


    1. 9241304
      03.06.2024 08:15

      Qt же


      1. sshmakov
        03.06.2024 08:15

        Да, но тоже с ограничениями

        • классы являются наследниками QObject

        • проперти, слоты и сигналы объявлены через ключевые слова или макросы

        • обычные методы недоступны в рантайм по имени

        Так что это также не сильно похоже на рефлексию в Java, где можно найти любой метод или поле любого класса.


        1. 9241304
          03.06.2024 08:15

          Читать так: Есть сторонняя библиотека RTTR (https://github.com/rttrorg/rttr), которая регистрирует свойства, методы и классы, после чего ими можно манипулировать.

          Я вот на это отвечал )


  1. sabudilovskiy
    03.06.2024 08:15
    +3

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

    Есть ли в c++ рефлексия? Пока - нет. Есть ли реализованная интроспекция для всех типов - нет. Но есть интроспекция для агрегатов, реализованная в boost.pfr. Лучше рассказать о ней, а не о двух морально устаревших библиотеках.


    1. 9241304
      03.06.2024 08:15
      +1

      Ну в зачаточном состоянии уже есть. И доступна через одно место. Но хоть что-то

      #include <rfl/json.hpp>
      #include <rfl.hpp>
      
      struct Person {
        std::string first_name;
        std::string last_name;
        int age;
      };
      
      const auto homer =
          Person{.first_name = "Homer",
                 .last_name = "Simpson",
                 .age = 45};
      
      // We can now write into and read from a JSON string.
      const std::string json_string = rfl::json::write(homer);
      auto homer2 = rfl::json::read<Person>(json_string).value();


      1. gyzl
        03.06.2024 08:15

        Выглядит как магия. Макросов в определении struct Person не видно. Как это работает?


        1. 9241304
          03.06.2024 08:15

          Всё по честному. Никаких макросов, только шаблоны. Но вся эта магия заканчивается на полях структуры и энамах. Для энамов с большими числами надо дополнительные параметры, чтобы всё работало. И насколько я помню, магия с ними доступна в виде отдельный либы уже давно


  1. ptr128
    03.06.2024 08:15
    +1

    Есть третий тип рефлексии, когда код формируется во время выполнения. И вот этот тип в C++ реализуется только вручную или используя библиотеку интерпретатора другого языка (например, Python), или компиляцией в процессе выполнения (так работает plrust в PostgreSQL)


  1. maxlarcenko
    03.06.2024 08:15

    Спасибо за статью, использую Unreal Engine, где используется рефлексия, но не знал, каким путем она достигается


    1. alohaeee
      03.06.2024 08:15

      По рефлексии в анриале есть статья
      https://habr.com/ru/articles/690662/