На прошлой неделе в Софии, столице Болгарии, закончилась работа над стандартом C++26, который теперь включает все наши предложения по рефлексии:

  1. P2996R13: Reflection for C++26

  2. P3394R4: Annotations for Reflection

  3. P3293R3: Splicing a Base Class Subobject

  4. P3491R3: define_static_{string,object,array}

  5. P1306R5: Expansion Statements

  6. P3096R12: Function Parameter Reflection in Reflection for C++26

  7. P3560R2: Error Handling in Reflection

Эти предложения перечислены в порядке их принятия, а не по степени влияния (иначе я бы поставил Splicing a Base Class Subobject последним). Это невероятное достижение, которое стало возможным благодаря работе множества людей, но главная заслуга в принятии рефлексии в C++26 принадлежит Дэну Кацу (LinkedIn, GitHub).

Сегодня я хочу рассказать о замечательном примере, который Дэн создал во время перелёта из Софии, пока я спал в нескольких креслах от него: возможность на этапе компиляции обработать JSON-файл и преобразовать его в объект C++. Например, для файла test.json следующего содержания:

{
    "outer": "text",
    "inner": { "field": "yes", "number": 2996 }
}

Можно написать такой код:

constexpr const char data[] = {
    #embed "test.json"
    , 0
};

constexpr auto v = json_to_object<data>;

И в результате получить объект v с следующим типом:

struct {
    char const* outer;
    struct {
        char const* field;
        int number;
    } inner;
};

Так что следующие проверки выполнятся успешно:

static_assert(v.outer == "text"sv);
static_assert(v.inner.number == 2996);
static_assert(v.inner.field == "yes"sv);

И это просто невероятно круто (godbolt).

Далее в этой статье мы разберём, как добиться такого результата. Я попытался переписать пример, используя Boost.JSON, поскольку парсинг JSON — не самая интересная его часть. Однако Boost.JSON не поддерживает парсинг на этапе компиляции. То же самое касается и nlohmann::json. Поэтому для наглядности я буду делать вид, что Boost.JSON работает, но вы можете посмотреть реальный код по ссылке на Compiler Explorer.

Начнём с простого

Попробуем спарсить JSON-объект, содержащий только одну пару ключ-значение, где значение это int.

consteval auto parse(std::string_view key, int value) -> std::meta::info;

То есть, для JSON вроде {"x": 1} нам нужно получить следующий тип:

struct S {
    int x;
};

И создать объект:

S{1}

Начнем с получения желаемого поля структуры (int x для {"x": 1}) и константы для его инициализации:

consteval auto parse(std::string_view key, int value)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
    auto init = reflect_constant(value);

    // ...
}
Примечание

Мы оборачиваем оба значения в reflect_constant, хотя data_member_spec уже является meta::info. Это потому, что вскоре нам нужно будет развернуть один слой рефлексии.

У нас есть поле, но теперь нам нужно объявить сам класс — для этого используется функция define_aggregate. Поскольку наша функция parse не шаблонная, нам нужен уникальный тип для каждого поля (ведь {"x": 1} и {"y": 1} должны порождать разные типы). Хитрое решение (спасибо Дэну) выглядит так:

template <std::meta::info ...Ms>
struct Outer {
    struct Inner;
    consteval {
        define_aggregate(^^Inner, {Ms...});
    }
};

template <std::meta::info ...Ms>
using Cls = Outer<Ms...>::Inner;

consteval auto parse(std::string_view key, int value)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
    auto init = reflect_constant(value);

    auto type = substitute(^^Cls, {member});
    // ...
}

Функция substitute(^^Z, {^^Args...}) возвращает ^^Z<Args>. То есть, имея рефлексию шаблона и рефлексии его аргументов, мы получаем рефлексию специализации. Это то, что я имел в виду, говоря о снятии одного слоя рефлексии: member должен быть рефлексией, представляющей data_member_spec, чтобы мы могли напрямую инстанцировать Cls с рефлексиями data_member_spec.

Теперь type — это рефлексия, представляющая уникальный тип для каждого набора нестатических полей. Важно, что этот тип автоматически «дедуплицируется» (для одинаковых полей будет получен одинаковый тип) и имеет внешнее связывание.

У нас есть тип (type) и инициализатор (init), осталось их объединить. Это ещё один вызов substitute. Как можно заметить, substitute — одна из самых полезных функций в API библиотеки:

template <std::meta::info ...Ms>
struct Outer {
    struct Inner;
    consteval {
        define_aggregate(^^Inner, {Ms...});
    }
};

template <std::meta::info ...Ms>
using Cls = Outer<Ms...>::Inner;

template <class T, auto... Vs>
inline constexpr auto construct_from = T{Vs...};

consteval auto parse(std::string_view key, int value)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    auto member = reflect_constant(data_member_spec(^^int, {.name=key}));
    auto init = reflect_constant(value);

    auto type = substitute(^^Cls, {member});
    return substitute(^^construct_from, {type, init});
}

Теперь наш код успешно пройдет следующие проверки:

static_assert([: parse("x", 1) :].x == 1);
static_assert([: parse("y", 2) :].y == 2);

Может показаться, что это мелочь, но мы создаем два разных типа с разными полями. Это довольно круто.

От одного ко многим

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

Ранее у нас было один member и один init с типом std::meta::info. Теперь они должны стать std::vector<std::meta_info>:

consteval auto parse(std::string_view key, int value)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits;

    members.push_back(reflect_constant(
        data_member_spec(^^int, {.name=key})));
    inits.push_back(reflect_constant(value));

    auto type = substitute(^^Cls, members);
    inits.insert(inits.begin(), type);
    return substitute(^^construct_from, inits);
}

Мы вставили type в начало inits, поскольку construct_from сначала требует тип, а затем все инициализаторы. Но мы можем сделать чуть лучше, добавив в inits заглушку и заменив её позже:

consteval auto parse(std::string_view key, int value)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    members.push_back(reflect_constant(
        data_member_spec(^^int, {.name=key})));
    inits.push_back(reflect_constant(value));

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

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

Полный JSON

Как я упомянул ранее, Boost.JSON не работает в constexpr контексте. Но цель этой статьи — не показать, как парсить JSON, а как преобразовать JSON-объект в структуру C++. Поэтому я буду делать вид, что Boost.JSON работает.

Вместо string_view и int возьмём boost::json::object и переберём все пары ключ-значение:

consteval auto parse(boost::json::object object)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    for (auto const& [key, value] : object) {
        // ...
    }

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

Теперь value — это boost::json::value, который может быть хоть JSON объектом, хоть массивом, хоть строкой, хоть числом, хоть константой. Но для простоты предположим, что он может быть только (1) числом, (2) строкой или (3) объектом.

Итак, случай с числом нам уже знаком, здесь нет ничего нового. Единственный сложный момент — в какой тип C++ отображать JSON-овое число? Согласно спецификации, они могут быть сколь-угодно большими. Но не будем усложнять себе жизнь и предположим, что int хватит всем.

consteval auto parse(boost::json::object object)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    for (auto const& [key, value] : object) {
        if (value.is_number()) {
            members.push_back(reflect_constant(
                data_member_spec(^^int, {.name=key})));
            inits.push_back(reflect_constant(
                value.to_number<int>()))
        } else {
            // ...
        }
    }

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

Заодно реализуем функцию add_member, чтобы в дальнейшем меньше повторяться:

consteval auto parse(boost::json::object object)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    for (auto const& [key, value] : object) {
        auto add_member = [&](std::meta::info type){
            members.push_back(reflect_constant(
                data_member_spec(type, {.name=key})));
        };

        if (value.is_number()) {
            add_member(^^int);
            inits.push_back(reflect_constant(
                value.to_number<int>()))
        } else {
            // ...
        }
    }

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

Теперь строки. Для их представления в итоговом типе вполне сгодится тип const char*, а инициализатор для них мы можем получить с помощью функции std::meta::reflect_contant_string, преобразующей строку в рефлексию static constexpr char[] с нуль-терминированным содержанием, соответствующим содержанию переданной строки.

consteval auto parse(boost::json::object object)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    for (auto const& [key, value] : object) {
        auto add_member = [&](std::meta::info type){
            members.push_back(reflect_constant(
                data_member_spec(type, {.name=key})));
        };

        if (value.is_number()) {
            add_member(^^int);
            inits.push_back(reflect_constant(
                value.to_number<int>()))
        } else if (auto s = value.if_string()) {
            add_member(^^char const*);
            inits.push_back(std::meta::reflect_constant_string(*s));
        } else {
            // ...
        }
    }

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

Наконец, случай с объектом. И тут мы, с чувством полного удовлетворения, можем пожинать плоды своих трудов: мы уже написали функцию, принимающую объект JSON и возвращаюущую рефлексию типа C++ — собственно, наш parse. Чтобы парсить объект, просто рекурсивно её вызовем:

consteval auto parse(boost::json::object object)
    -> std::meta::info
{
    using std::meta::reflect_constant;

    std::vector<std::meta::info> members;
    std::vector<std::meta::info> inits = {^^void};

    for (auto const& [key, value] : object) {
        auto add_member = [&](std::meta::info type){
            members.push_back(reflect_constant(
                data_member_spec(type, {.name=key})));
        };

        if (value.is_number()) {
            add_member(^^int);
            inits.push_back(reflect_constant(
                value.to_number<int>()))
        } else if (auto s = value.if_string()) {
            add_member(^^char const*);
            inits.push_back(std::meta::reflect_constant_string(*s));
        } else {
            std::meta::info inner = parse(value.as_object());
            add_member(remove_const(type_of(inner)));
            inits.push_back(inner);
        }
    }

    inits[0] = substitute(^^Cls, members);
    return substitute(^^construct_from, inits);
}

Вот и всё. Ну, или было бы всё, если бы Boost.JSON умел в constexpr.

Последние штрихи

В реальной реализации функция parse_json принимает string_view и действительно парсит весь JSON:

consteval auto parse_json(std::string_view json) -> std::meta::info {
    // код
}

На этом мы могли бы и остановиться, но приправим нашу библиотеку красивым интерфейсом:

struct JSONString {
    std::meta::info Rep;
    consteval JSONString(const char *Json) : Rep{parse_json(Json)} {}
};

template <JSONString json>
consteval auto operator""_json() {
    return [:json.Rep:];
}

template <JSONString json>
inline constexpr auto json_to_object = [: json.Rep :];

Вот и всё. Теперь мы можем загрузить произвольный JSON-файл с помощью директивы #embed (также принятой в C++26) и сразу же превратить его в объект C++:

constexpr const char data[] = {
    #embed "test.json"
    , 0
};

constexpr auto v = json_to_object<data>;

Или даже работать напрямую со строковыми литералами, используя определенный нами пользовательский литерал:

static_assert(
    R"({"field": "yes", "number": 2996})"_json
    .number == 2996);

По сути, мы реализовали аналог JSON type providers из F# в виде довольно компактной библиотеки. Интерфейс, конечно, не совсем такой же, но напильничком по нашей реализации пройтись — и будет не отличить.

(Reflection is) a whole new language.

— Hana Dusíková at June 2025 ISO C++ standards meeting (Sofia, Bulgaria)

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


  1. vadimr
    28.06.2025 14:43

    Чувак захардкодил данные в исполняемый файл и почему-то считает, что это круто.

    Хотя исходно смысл рефлексии в программировании прямо противоположный - менять код при выполнении программы.


    1. sabudilovskiy
      28.06.2025 14:43

      Как сказать, что ты программируешь только на какой-нибудь Java, не говоря об этом.

      Кто виноват, что у вас такой плохой язык, что он не в состоянии такое сделать на компиляции? что у вас ошибки рефлексии можно увидеть ТОЛЬКО на рантайме


      1. Kotofay
        28.06.2025 14:43

        На Java гнать волну не стоит.

        Смысл хардкодить из json во время компиляции? Проще тогда сразу средствами языка всё описать и в include подключить.
        А рефлексия во время выполнения это средство расширения функционала без переписывания кода.


        1. sabudilovskiy
          28.06.2025 14:43

          Кто тебе сказал, что это нельзя применить и в рантайме? Средство расширение функционала - какого? 99% кода рефлексии на Java делает тупо обход всех полей и сериализацию

          Вы просто реально достали, у вас ущербное мышление, что динамическая рефлексия - это вся рефлексия, которая возможна, хотя динамическую рефлексию при желании можно реализовать через статическую, но не наоборот.


          1. vadimr
            28.06.2025 14:43

            Представьте, что ваш json меняется пользователем в произвольные моменты времени. Попробуйте это реализовать через "статическую рефлексию".


            1. Gorthauer87
              28.06.2025 14:43

              Чаще всего он не меняется и лучше его описать средствами типов языка. А если в нем что-то меняющееся есть, то можно эти поля оставить с типом json_value например.


          1. Kotofay
            28.06.2025 14:43

            Это вы тут реально достали своим ущербным мышлением в парадигме статической рефлексии.


          1. slonopotamus
            28.06.2025 14:43

            Кто тебе сказал, что это нельзя применить и в рантайме?

            Там всё в consteval'ах.


        1. eao197
          28.06.2025 14:43

          Смысл хардкодить из json во время компиляции? Проще тогда сразу средствами языка всё описать и в include подключить.

          А если этот json производится какой-то внешней тулзиной, к исходникам которой у вас даже доступа нет?


          1. vadimr
            28.06.2025 14:43

            Откуда тогда уверенность, что он не изменится за время эксплуатации вашей программы?


            1. DungeonLords
              28.06.2025 14:43

              Вот у нас ровно так и делается. Есть программа на Java и программа на Qt. И нужно создать общий так сказать header file из которого обе программы будут брать актуальный для данного релиза поток данных. Динамически подгружать - только ошибки плодить. А синтаксис у языков разный, JSON бы спас


              1. vadimr
                28.06.2025 14:43

                Перекомпилировать программу из-за изменения данных – жуткий костыль. Так-то ничего в аргументации по поводу хардкодинга не изменилось за последние 70 лет. И именно для того, чтобы не нужно было зашивать структуру данных в исполняемый модуль, но в то же время необязательно было бы писать настоящий самомодифицирующийся код (что возможно далеко не во всех языках) и была придумана рефлексия.


                1. Gorthauer87
                  28.06.2025 14:43

                  А постоянно менять протоколы обмена данных без обеспечения обратной совместимости это просто архитектурная безалаберность.


                  1. vadimr
                    28.06.2025 14:43

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


                    1. Gorthauer87
                      28.06.2025 14:43

                      А в рефлексии откуда она новая возьмется? Или сам json должен передавать логику своей обработки?

                      Но тогда это remote code execution vulnerability.


                      1. vadimr
                        28.06.2025 14:43

                        Даже если новая не возьмется, то старая не пропадет при реализации новой.


                      1. Gorthauer87
                        28.06.2025 14:43

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


                      1. vadimr
                        28.06.2025 14:43

                        Если делать так, как показано в статье, то частью кода программы автоматически станет преобразованная форма именно текущей версии интерфейсного файла на момент компиляции. А при традиционной рефлексии код можно было бы выбирать по версии файла на момент исполнения.

                        Отдельно добавлю про remote code execution vulnerability, про что не дописал вчера. Рассмотрим программу-компилятор. Вот уж remote code execution vulnerability какая здоровенная в нём, не правда ли? Или программа-терминал. Это я к тому, что remote code execution vulnerability подразумевает недружественное окружение для этого code, а не просто всякий случай исполнения пришедших со стороны инструкций. То есть всё зависит от задачи.


                1. KivApple
                  28.06.2025 14:43

                  При зашивании всего в программу, можно на этапе компиляции проверить данные вместо падения в рантайме. Плюс не надо писать обработку ошибок. Плюс не надо тратить ресурсы на парсинг при каждом запуске. Плюс не везде есть файловая система в привычном виде, чтобы из нее читать конфиг (embedded, wasm)


              1. Kotofay
                28.06.2025 14:43

                Нет гарантии что статическая рефлексия защищает от ошибок.


            1. eao197
              28.06.2025 14:43

              Во-первых, какой такой программы. В статье описан потенциальный механизм, который пока не реализуем из-за отсутствия полноценных compile-time парсеров (хоть JSON-а, хоть еще чего-нибудь). Т.е., с моей точки зрения, это нельзя рассматривать как готовый "рецепт", а просто как proof-of-concept для того, чтобы разработчики могли осознать мощь новых возможностей.

              Во-вторых, это в данном proof-of-concept из json-а генерируются объекты с конкретными значениями, что и дает вам возможность доколупаться до воображаемого вами хардкодинга. Но таким же образом могут генерироваться и структуры по внешним спецификациям (наприимер, из .proto-файлов или из описаний Swagger-а). И тогда содержимое внешних файлов спецификаций не может просто так поменяться. А если меняется, то под новую спецификацию и логику нужно будет подправить.

              Так что нет, фатальных проблем с "хардкодингом" я лично в тексте не вижу.


              1. vadimr
                28.06.2025 14:43

                Сам по себе принцип макрогенерации кода я ни в коем случае не ставлю под сомнение (хотя реализован он в C++ настолько криво, что пользоваться им в проде вряд ли кто-то решится, как верно замечено в комментарии ниже). Но конкретный пример даже в качестве proof-of-concept, с моей точки зрения, не имеет смысла. Это чем-то напоминает пресловутое объяснение ООП на примере наследования животных друг от друга.

                Вполне возможно, году к 2030 в компилятор C++ вкорячат полноценный парсер и генератор AST, который сможет делать почти всё как в Лиспе за 70 лет до того. Только не забыть бы в процессе, зачем вообще это было нужно. А вроде уже забыли, судя по таким примерам.


                1. eao197
                  28.06.2025 14:43

                  Но конкретный пример даже в качестве proof-of-concept, с моей точки зрения, не имеет смысла.

                  Ваше мнение очень важно для всех нас, продолжайте держать нас в курсе.

                  всё как в Лиспе за 70 лет до того

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

                  Только не забыть бы в процессе, зачем вообще это было нужно.

                  И зачем же?


                  1. vadimr
                    28.06.2025 14:43

                    И зачем же?

                    Ну вот я и спрашиваю, зачем.

                    С ваших слов, это

                    потенциальный механизм, который пока не реализуем

                    ***

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

                    Любите востребованность – пишите на Питоне. Там, кстати, и с рефлексией получше.


                    1. eao197
                      28.06.2025 14:43

                      Ну вот я и спрашиваю, зачем.

                      Простите, но я вот в этой вашей фразе "Вполне возможно, году к 2030 в компилятор C++ вкорячат полноценный парсер и генератор AST, который сможет делать почти всё как в Лиспе за 70 лет до того. Только не забыть бы в процессе, зачем вообще это было нужно. А вроде уже забыли, судя по таким примерам." вопроса не увидел.

                      Так вы знаете зачем нужна рефлексия или у меня спрашиваете?

                      С ваших слов, это

                      С моих слов это не это.

                      Статья показывает как взять некое описание и из этого описания сгенерировать набор C++ных структур + набор объектов с конкретными значениями. Что может быть использовано для ряда задач из категории eDSL. Например, генерация эффективных разборщиков регулярных выражений.

                      Или генераторов лексических/грамматических парсеров. Например, нам пришлось сделать свой парсер на базе PEG. Только для этого пришлось упороться шаблонами, что привело, как минимум, к двум неприятным последствиям:

                      1. Трудно это дело отлаживать.

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

                      В то же время, если в compile-time распарсить eDSL с тем же PEG выражением, то может получиться гораздо проще и удобнее.

                      Еще один потенциальный пример из нашей же истории. Использовать аналог express-js в C++ ну такое себе: работает, но часть ошибок, которые в потенциале можно было бы ловить прямо в compile-time, перекладывается на run-time. Можно сделать с большими гарантиями по безопасности по типам, но получается так себе по эргономике и сложно в реализации. Если же описание end-point-ов делать специальными строками в аннотациях методов, то можно и сильно улучшить эргономику, и (сильно надеюсь) упростить реализацию.

                      Это если говорить о грядущей рефлексии именно в стиле -- взять внешнее описание чего-нибудь и сгенерировать из него набор C++ных сущностей. Если же говорить о других применениях рефлексии, то из совсем свежих впечатлений: в C++ нет strong typedef и вряд ли в обозримом будущем появится. Но на базе рефлексии можно сделать собственный лисапед, который позволит написать что-то вроде:

                      using first_type = my_strong_typedef_for< std::map<int, std::string> >;
                      using second_type = my_strong_typedef_for< std::map<int, std::string> >;
                      
                      static_assert(!std::is_same_v<first_type, second_type>);
                      

                      Любите востребованность – пишите на Питоне.

                      Не говорите людям что им делать и им не придется говорить вам куда отправиться.


                      1. vadimr
                        28.06.2025 14:43

                        Зачем нужна рефлексия, вы можете прочитать в интернете, набрав что-нибудь вроде "рефлексия в программировании". А именно, для того, чтобы программа могла исследовать и менять свою собственную структуру на этапе выполнения, приспосабливая её к своим входным данным. А "рефлексия на этапе компиляции" – это просто оксюморон. Для компилятора сам текст программы является входными данными.

                        Не надо было авторам весьма ограниченный по своим возможностям (да и вообще хоть какой) макропроцессор называть "рефлексией", не было б споров.

                        Воообще, хоть в философии, хоть в психологии, хоть в программировании слово "рефлексия" обозначает по сути одно и то же – действия или мысли отражаются (reflect) в каком-то зеркале, и это отражение служит основой для новых действий или мыслей, замыкая цикл обратной связи. В компиляции же никакого отражения и цикла обратной связи нет, потому что это действие однократно.

                        Те примеры, которые вы привели, являются просто обычными парсерами, подключаемыми на этапе компиляции.


                      1. eao197
                        28.06.2025 14:43

                        А именно, для того, чтобы программа могла исследовать и менять свою собственную структуру на этапе выполнения, приспосабливая её к своим входным данным.

                        Вы смешиваете в одну кучу и рефлексию, и динамическую кодонерацию, и горячую замену кода.

                        А "рефлексия на этапе компиляции" – это просто оксюморон.

                        И? С++никам теперь стоит собраться и написать пропозал в комитет для изъятия рефлексии из языка потому что г.@vadimr объявил её оксюмороном в комментариях на Хабре?

                        Для компилятора сам текст программы является входными данными.

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

                        Те примеры, которые вы привели, являются просто обычными парсерами, подключаемыми на этапе компиляции.

                        Во-первых, не всегда удобно подключать что-то внешнее на этапе компиляции. Именно поэтому мы и стали делать свой PEG-парсер.

                        Во-вторых, покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции".


                      1. vadimr
                        28.06.2025 14:43

                        Во-вторых, покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции".

                        Синтаксис свой определить, тоже мне фокус. Хотя сама цель сомнительная.

                        (define-syntax typedef
                          (syntax-rules ()
                             ((_ tag val) `(,'tag ,val))))

                        И? С++никам теперь стоит собраться и написать пропозал в комитет для изъятия рефлексии из языка потому что г.@vadimr объявил её оксюмороном в комментариях на Хабре?

                        Ну неплохо было б. Но комитет C++ стандарты принимает же без учёта мнения специалистов, в отличие от некоторых других языков.


                      1. eao197
                        28.06.2025 14:43

                        Синтаксис свой определить, тоже мне фокус.

                        Каким боком ваши упражнения в Лиспе относятся к C++?

                        Ну неплохо было б.

                        Годы идут, лисперы не меняются.


                      1. vadimr
                        28.06.2025 14:43

                        Каким боком ваши упражнения в Лиспе относятся к C++?

                        Никаким, к сожалению.

                        Они относятся к вашему вопросу “покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции"”. И иллюстрируют тот факт, что никакой анализ текста программы, как бы его не называть, для этого не нужен.


                      1. eao197
                        28.06.2025 14:43

                        Никаким, к сожалению.

                        Статья о том, что сделано в C++ для того, чтобы решать проблемы C++ников, но вы пришли сюда что бы порассуждать о том, как может быть в других языках? Конструктивно, что тут еще скажешь.

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

                        Тогда покажите как это сделать именно в C++. Ведь не нужен же это, как его, "никакой анализ текста программы". Вот и продемонстрируйте мастер-класс.


                      1. vadimr
                        28.06.2025 14:43

                        Тогда покажите как это сделать именно в C++

                        Самым разумным решением, если потянуло на рефлексию, будет что-то вроде

                        execv ("clisp", argv);


                1. KivApple
                  28.06.2025 14:43

                  Это может быть конфиг. Активированные в билде фичи, версия, хеш коммита и т д. Они не должны и не могут меняться после сборки.

                  Раньше для такого приходилось писать кодогенератор, теперь можно использовать для конфига JSON без предобработки, который гораздо проще редактировать из других скриптов, чем исходник.

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

                  То что вы не можете придумать зачем фича, это ещё не значит, что она не нужна.


                  1. vadimr
                    28.06.2025 14:43

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

                    В чём практический смысл?


                    1. KivApple
                      28.06.2025 14:43

                      В том, что по ТЗ не нужна динамическая замена ресурсов.

                      Это может быть embedded, где файловой системы вообще нет и всё вкомпилировано в бинарник. Это может быть WASM, где такое тоже встречается. Это может быть портативный софт "всё в одном бинарнике". Динамическая погрузка ресурсов не бесплатна, не всегда возможна и не всегда нужна.

                      Просто нужно иметь возможность вкомпилировать в бинарник некие const данные, которые не будут меняться после сборки. Раньше надо было описывать такие данные в коде, тепеоь можно принять данные в любом удобном формате без дополнительных утилит, кроме компилятора.


                      1. slonopotamus
                        28.06.2025 14:43

                        Ну вот вы приводите пример с хешом коммита. И как его при помощи машинерии из статьи засунуть в бинарник? Откуда возьмётся файл с хешом? Генерировать внешним скриптом? Ну так тогда можно сразу c++-файл сгенерировать.


                      1. KivApple
                        28.06.2025 14:43

                        Я не случайно привёл в пример не просто один хеш коммита, а целый конфиг сборки.

                        Допустим, у нас есть какой-то редактор этого конфига и проще сохранять/загружать его в json, чем парсить какой-нибудь config.h. Особенно если конфиг сложный, содержит вложенные структуры и т д

                        Или мы качаем конфиг с внешнего сервиса, где его меняют через веб интерфейс люди вообще далёкие от программирования на плюсах, а CI/CD запускает сборку бинарника по нему.

                        Да, это всё уже сейчас решается кодогенерацией. Но это надо писать скрипты под конкретный проект, усложнять сборку и т д. А теперь можно просто положить JSON и компилятор сам всё сделает. Разумеется, constexpr парсер json должен быть из какой-то библиотеки, а не руками его писать.


            1. KivApple
              28.06.2025 14:43

              Его может вообще не быть на этапе исполнения программы, он становится не нужен после сборки.


        1. KivApple
          28.06.2025 14:43

          JSON может гененироваться внешней утилитой во время сборки, приходить из другой системы или писаться коллегой, который вообще не умеет программировать на C++.

          Допустим, бекэндеры описывают в JSON какую-то схему данных, а в нашем приложении автоматически генерируется нужные DTOшки под неё во время сборки.

          Раньше требовалось писать отдельный кодогенератор, а теперь можно это провернуть средствами компилятора.


          1. slonopotamus
            28.06.2025 14:43

            Раньше требовалось писать отдельный кодогенератор, а теперь можно это провернуть средствами компилятора.

            Раньше требовалось писать кодогенератор, а теперь придётся писать кодогенератор.


            1. Gorthauer87
              28.06.2025 14:43

              Ну в целом это он и есть, зато вот прямо рядом и в самом языке. Это вполне удобно потребность в этом была понятна ещё с момента изобретения moc в Qt. Через него, кстати, генерируется код для рантайм рефлексии как в Java.


            1. KivApple
              28.06.2025 14:43

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


              1. Mingun
                28.06.2025 14:43

                Да, но тогда вопрос, зачем называть это "рефлексией", когда за таким названием закрепились вполне определенные ассоциации, и зачем было делать новый синтаксис? Изучить опыт Rust-а и писать модули к компилятору на обычном С++, а не на специально для этого придуманном подмножестве не судьба?


      1. rutexd
        28.06.2025 14:43

        Даже без Java, делать подобные внешние определения - такое себе. Добавляет пространство для ошибок, уменьшает гибкость. Не видно во что компилируется макрос - надо лезть в внешний файл и убивает анализаторы кода.

        Идея хорошая, поиграться / протестировать - за, в прод - боже упаси.


    1. pnmv
      28.06.2025 14:43

      "менять свой собственный код в ходе выполнения программы" - всегда считал, что это "динамическое программирование".


      1. vadimr
        28.06.2025 14:43

        Динамическое программирование - это вообще из другой оперы. Восходящее решение подзадач.


        1. pnmv
          28.06.2025 14:43

          Я тонко намекнул на давнюю моду - называть одно и то же разными именами.

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


      1. slonopotamus
        28.06.2025 14:43

        Ну кагбэ:

        Рефлексия (отражение; холоним интроспекции, англ. reflection) — процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.


        1. pnmv
          28.06.2025 14:43

          Это всё вокруг одного и того же.


        1. Jijiki
          28.06.2025 14:43

          если проект попроще чем очень большой или разбит на части, то еслиб репл ClangRepl.html(во точно репл для С++ с рефлексией и отключаемыми модулями или кодом прям на запущенном репле) работал почти как sly(или таже идея с clojure) то что-то около этого было бы удобно вроде, но только в случае удобной компиляции и прочее

          запустил пачку json-ов и смотришь как она поочереди их запускает реплом посути, ну и там придётся всё равно под пачку json-ов что-то делать, но логично вроде


        1. eao197
          28.06.2025 14:43

          программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.

          Т.е. когда вот такой код модифицирует структуру программы:

          consteval auto parse(std::string_view key, int value)
              -> std::meta::info
          {
              using std::meta::reflect_constant;
          
              std::vector<std::meta::info> members;
              std::vector<std::meta::info> inits;
          
              members.push_back(reflect_constant(
                  data_member_spec(^^int, {.name=key})));
              inits.push_back(reflect_constant(value));
          
              auto type = substitute(^^Cls, members);
              inits.insert(inits.begin(), type);
              return substitute(^^construct_from, inits);
          }
          

          то никакого "выполнения", надо полагать, нет. Получается, что программист написал код на C++, этот код выполнился, породил в процессе своей работы новые структуры данных и объекты, но за "выполнение" это не считается. Вероятно, функция parse в коде не вызывается, и результат ее работы ни к чему не приводит. Вот прям "не верь глазам своим" (С) К.Прудков.


          1. slonopotamus
            28.06.2025 14:43

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


            1. eao197
              28.06.2025 14:43

              Одна выполняется во время компиляции (макросы тоже например)

              Макросы не в тему от слова совсем.
              Есть программа, она выполняется, в процессе выполнения меняет себя. Да, в compile-time. Поэтому и называется compile-time рефлексия.

              Но выполнение есть. Что и требовалось в том определении, на которое вы сослались.


    1. Jijiki
      28.06.2025 14:43

      так компил базу данных можно а туже аст в json нельзя чтоли? тоесть она получая АСТ может еще как возможность код и в json по красоте кидать(наверно ну просто звучит как сериализация) что плохого-то


  1. rPman
    28.06.2025 14:43

    Хм... на основе .proto файлов нужно создавать классы


  1. TimurZhoraev
    28.06.2025 14:43

    Вообще говоря выглядит как хороший полуфабрикат для генерации чего бы то ни было с использованием некоего PerlScript-а, который штампует заголовочники с #embed. Потенциально настроечно-конфигурационные макросы и CMAKE будут хорошенько пересмотрены. На очереди встроенный Bison-Flex и Regexp как вишенка.


  1. 9241304
    28.06.2025 14:43

    Я так понимаю, у бустовиков вообще не принято смотреть по сторонам. Их либа ну самая топовая, ага. Сравнивать можно только с ней. Потом появляются такие вот открытия... Стандарт ещё не вышел, а они уже мячьтають)))

    Будет ещё время помечтать, детишки. Стандарт в лучшем случае получится заюзать года через 4. Пока что можно не заниматься ерундой, а заюзать reflect-cpp, и поубивать наконец ваши бустовые лохмотья на 100к строк. А ещё лучше реализовать как минимум сетевую часть вашего приложения на более дружелюбном языке, а не на плюсах и не дай бог на этом вашем бусте беаст и асио)


    1. SilverTrouse
      28.06.2025 14:43

      Рефлексия это не только json, proto. Рефлексия открывает дорогу к полноценной замене moc. Замена moc вместе с введением контрактов в том с++26 уже открыввает модуляризацию и упрощению компиляции большего числа существующих проектов . Вы не думали об этом?


  1. KivApple
    28.06.2025 14:43

    Люди, жалующиеся на то, что рефлексия не рантайм. На статической рефлексии легко реализовать рантайм (собрать все поля и методы класса в служебные структуры, позволяющие косвенное обращение, а затем в рантайме их использовать). А вот наоборот не выйдет.

    Так что статическая рефлексия является более общим и универсальным механизмом.


    1. vadimr
      28.06.2025 14:43

      Это верно только для самого простого и бесполезного случая - перестановки полей.

      В рантайме вы можете, например, средствами рефлексии обращаться к коду, которого на этапе компиляции еще не существовало, как драйверы jdbc в Джаве.

      Конечно, в конечном итоге всё в большинстве случаев можно переписать на машинном коде без рефлексии и без самомодификации, но вопрос-то не в этом.

      А вот наоборот не выйдет. 

      Любая операция времени компиляции может быть с тем же результатом выполнена в рантайме. В принципе, ничто не мешает реализовать целиком интерпретатор C++.


      1. KivApple
        28.06.2025 14:43

        Ваще не проблема. Пишете свой класс Object (или используете какую-то библитеку), которые имеет виртуальные методы типа getMethods, getFields и от которого наследуются все классы, которые должны уметь в рефлексию. При этом реализация этих методов генерируется в момент компиляции соответствующего класса с помощью статической рефлексии в одну строчку (вызов какой-нибудь consteval функции или наследованием через особый хитрый шаблон).

        Теперь вы можете обращаться через такую рефлексию к классам из других модулей, к которым у вас не было доступа во время сборки вашего собственного кода (ну, конечно, собрано оно должно быть компиляторами с одинаковым ABI, это уже ограничение самих плюсов). Главное, чтобы код Object и генератора метаданных был общий для всех (благодаря статической рефлексии он может быть в виде header-only библиотеки, которую ничего не стоит подрубить везде).

        Что-то подобное уже реализовано в Qt (там можно вызывать методы и писать свойства объектов по их именам), но требует запуска специального препроцессора для генерации метаданных. Со статической рефлексией можно отказаться от отдельного компилятора, снизив порог входа и упростив использование.


        1. vadimr
          28.06.2025 14:43

          Ну правильно, это и будет сделанная руками рефлексия. Только она не будет поддерживаться языком, и, как следствие, вам придётся везде наследоваться от этого вашего Object и руками писать косвенные вызовы. Также можно руками реализовать VMT и иметь ООП на голом Си и т.д.

          Но при этом у вас возникнет две иерархии классов – родная плюсовая и ручная рефлективная, и расхождение между ними будет трудно обнаружимо.


          1. KivApple
            28.06.2025 14:43

            Средства языка позволяют достаточно сахара для того, чтобы спрятать косвенные вызовы функции. Есть же шаблоны и перегрузка операторов. Можно написать MethodRef, который будет вызываться точь в точь как настоящий метод, только без проверки аргументов в compile time (обернуть все в std::any или аналог) перегружая operator() variadic template функцией.

            На самом деле, видимым в хорошей реализации останется только наследование от Object. Но это одна строчка на класс, плюс философия плюсов не платить за то чем ты не пользуешься, так что даже на уровне языка если бы была динамическая рефлексия, было бы нужно её явно включать для каждого класса специальным ключевым словом. Потому что она не бесплатна в рантайме (по сути сделает обязательной vtable для класса).