Рефлексия и интроспекция в C++: Как заставить код видеть себя

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое рефлексия и интроспекция в C++ и зачем это нужно?

Представьте, что вы пишете сложный игровой движок или фреймворк для обработки данных. В какой-то момент вам нужно понять: «А что это за объект вообще? Какие у него методы? Есть ли у него поле name?»

В языках вроде Python или Java это делается одной строкой — там есть полноценная рефлексия (способность программы изучать и изменять свою структуру во время выполнения). А вот в C++ всё сложнее. C++ — язык с сильной статической типизацией, и «из коробки» рефлексии почти нет. Но есть интроспекция — ограниченная способность получать информацию о типах на этапе выполнения.

В этой статье мы разберём:

  • Чем отличается рефлексия от интроспекции;
  • Как работает RTTI (Run-Time Type Information);
  • Как использовать typeid и dynamic_cast;
  • Как эмулировать рефлексию с помощью шаблонов и макросов;
  • Современные альтернативы (C++20/23) и библиотеки.

Готовы заглянуть в «зазеркалье» C++? Поехали!

Интроспекция vs Рефлексия: в чём разница?

Прежде чем писать код, давайте расставим точки над i.

  • Интроспекция — это возможность программы «посмотреть на себя» и получить информацию о типах, но не изменять структуру программы. Например: «Узнать имя класса объекта». Это есть в C++ через RTTI.
  • Рефлексия — это интроспекция + возможность динамически создавать объекты, вызывать методы, изменять поля. В C++ этого нет в стандарте (пока).

В C++ мы используем термин «рефлексия» чаще как цель, а «интроспекция» — как доступный инструмент. Но не пугайтесь: даже с ограниченными средствами можно делать крутые вещи.

RTTI (Run-Time Type Information) — основа интроспекции в C++

RTTI — это механизм, который позволяет определить тип объекта во время выполнения. Включается он по умолчанию (хотя в некоторых встраиваемых системах его отключают флагом -fno-rtti).

Главные игроки RTTI:

  • typeid — оператор, возвращающий std::type_info;
  • dynamic_cast — безопасное приведение полиморфных типов.

Пример 1: Использование typeid

#include <iostream>
#include <typeinfo>

class Base { virtual void foo() {} };
class Derived : public Base {};

int main() {
    Base* b = new Derived();
    std::cout << "Тип объекта: " << typeid(*b).name() << std::endl;
    delete b;
    return 0;
}

Вывод (зависит от компилятора): class Derived или 7Derived (искажённое имя).

Обратите внимание: typeid работает только с полиморфными классами (имеющими хотя бы один виртуальный метод). Если убрать virtual, вы получите тип указателя, а не реальный объект.

Пример 2: dynamic_cast — безопасное приведение

#include <iostream>

class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal { public: void bark() { std::cout << "Woof!\n"; } };
class Cat : public Animal { public: void meow() { std::cout << "Meow!\n"; } };

void makeSound(Animal* a) {
    if (Dog* dog = dynamic_cast<Dog*>(a)) {
        dog->bark();
    } else if (Cat* cat = dynamic_cast<Cat*>(a)) {
        cat->meow();
    } else {
        std::cout << "Unknown animal\n";
    }
}

int main() {
    Animal* pets[] = { new Dog(), new Cat(), new Animal() };
    for (auto p : pets) makeSound(p);
    // Не забываем удалить!
    for (auto p : pets) delete p;
    return 0;
}

Это классический пример интроспекции: мы не знаем, какой именно объект пришёл, но можем это выяснить и вызвать правильный метод.

Ограничения RTTI и почему это не «настоящая» рефлексия

RTTI даёт только имя типа и позволяет проверить иерархию наследования. Но вы не можете:

  • Получить список полей класса;
  • Динамически вызвать метод по строковому имени;
  • Создать объект по имени класса.

Для этого нужна либо ручная эмуляция, либо внешние библиотеки.

Эмуляция рефлексии в C++: макросы и шаблоны

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

Пример: простой рефлектор для полей

#include <iostream>
#include <string>
#include <vector>
#include <functional>

// Простая структура для хранения информации о поле
struct FieldInfo {
    std::string name;
    size_t offset;
    size_t size;
};

// Макрос для регистрации поля
#define REFLECT_FIELD(Class, Field) \
    { #Field, offsetof(Class, Field), sizeof(((Class*)nullptr)->Field) }

// Пример класса с рефлексией
class Person {
public:
    std::string name;
    int age;
    
    static std::vector<FieldInfo> getFields() {
        return {
            REFLECT_FIELD(Person, name),
            REFLECT_FIELD(Person, age)
        };
    }
};

int main() {
    Person p{"Alice", 30};
    auto fields = Person::getFields();
    for (const auto& f : fields) {
        std::cout << "Поле: " << f.name 
                  << ", размер: " << f.size << " байт" << std::endl;
    }
    return 0;
}

Это уже похоже на рефлексию: мы можем получить список полей класса. Однако это всё ещё статическая информация, собранная на этапе компиляции.

Современные подходы: C++20/23 и будущее рефлексии

Хорошие новости: в C++26 (или даже раньше) ожидается введение статической рефлексии. Уже есть экспериментальные реализации:

  • Boost.PFR (Boost.Preprocessor + Reflection) — позволяет работать со структурами как с кортежами;
  • magic_get — библиотека для доступа к полям агрегатных типов;
  • REFLECT от Google — мощный инструмент для сериализации.

Пример с Boost.PFR (до C++20)

Блоги

Рекомендаций по книгам