Определение понятия "рефлексия" из Википедии:

In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.

В последние годы разрабатываются варианты ввода рефлексии в стандарт C++.

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

Рефлексия в других языках

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

В языке Python в run-time можно получить класс объекта; имя класса; все его методы и аттрибуты; добавить методы и аттрибуты в класс; и так далее. По большому счету, каждый объект и класс это dict (с синтаксическим сахаром), который можно изменять как угодно.

В языке Java в run-time также можно получить класс объекта; его поля, методы, константы, конструкторы, суперклассы; получать и устанавливать значение поля по его имени; вызывать метод объекта по имени; и так далее. Информация о классах находится в памяти Java Virtual Machine.

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

Эрзац-рефлексия в C++

В C++ постепенно добавлялись некоторые магические кусочки "языкознания", с помощью которых можно получить часть информацию о коде - например std::is_enum (compile-time), typeid (run-time).

Это можно отнести к рефлексии, но функционал спартанский и для великих свершений не годен. История знает разного рода приспособления для уменьшения боли.

Кодогенерация по описанию типа данных

К этому типу принадлежит protobuf - модный "носитель" данных.

В .proto-файле описывается структура данных (message Person), по которой кодогенератор для C++ может создать соответствующий ей класс (class Person) с геттерами/сеттерами, и возможностью сериализовать эти данные без копипаста имени каждого метода.

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

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

Адские макросы и шаблоны

К этому типу принадлежит библиотека Boost.Hana. Для нее нужно описывать структуру нужным образом:

struct Person {
  BOOST_HANA_DEFINE_STRUCT(Person,
    (std::string, name),
    (int, age)
  );
};

Макрос "раскроется" и все сгенерирует. Похоже на "демосцену" - выжимают максимум возможностей из инструмента, который не был для этого предназначен.

Экстремальный залаз в компилятор

Интересные вещи можно сделать, проанализировав исходный код.

Некоторые инструменты (кодогенераторы/чекеры/etc.) создаются как "плагин" к используемому компилятору. Например, чтобы работать с исходниками на уровне AST (абстрактного синтаксического дерева), можно использовать cppast.

AST это промежуточный вариант между исходным кодом и ассемблером. К нему надо привыкнуть, но это проще, чем писать самодельный парсер кода на C++. Если кто-то смотрел исходники GCC или Clang, тот знает, что с нуля написать парсер малореально.

Особенности рефлексии в C++

В отличие от многих других языков, где с рефлексией работают в run-time, дух C++ требует сделать рефлексию в compile-time.

Так как язык старается соответствовать принципу "don’t pay for what you don’t use", то ~95% всей информации из исходников в рантайме просто испаряется. В языке не существует теоретической возможности сделать рефлексию в рантайме без раздувания бинаря чем-нибудь навроде RTTI (с многократно большим объемом).

C++ можно рассматривать как сборник из "под-языков", работающих на разных "уровнях". Условное деление:

  • Собственно C++: работа с памятью, объектами, потоками (и вообще с интерфейсом ОС), манипуляция данными. Работает в run-time.

  • Шаблоны: обобщенное программирование в исходниках. Работает (вычисляется) в compile-time.

  • Constexpr-вычисления: это "интерпретируемое" подмножество "Собственно C++", от года в год расширяется. Подробнее о них можно прочитать в моей прошлой статье. Вычисляется в compile-time прямо внутри компилятора.

  • Препроцессор: работает с токенами (отдельными словами) исходников. С C++ имеет очень посредственную связь, абсолютно такой же препроцессор могли бы сделать для Rust/Java/C#/etc. Единственный из "под-языков" не тьюринг-полный. Работает в compile-time.

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

Сначала рефлексию планировали ввести в шаблонной парадигме, сейчас планируют ввести в constexpr-парадигме (так как возможности constexpr значительно расширились).

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

Рефлексия на шаблонах

Основной источник информации про рефлексию на шаблонах - pdf-ка Reflection TS, более короткое объяснение есть на cppreference.com.

Свой код с использованием рефлексии можно скомпилировать на godbolt.org, выбрав компилятор x86-64 clang (reflection).

Вводится оператор reflexpr(X), которому можно "скормить" вместо X что угодно: тип, выражение, имя переменной, вызов метода, и т.д.

Этот оператор вернет так называемый meta-object type (далее - магический тип"), который для нас будет являться безымянным incomplete классом. Пример кода:

enum Color {
    Red, Green, Blue
};

using MetaT = reflexpr(Color);

Этот класс будет удовлетворять некоторому множеству концептов (в Reflection TS есть таблица концептов).

Например, MetaT удовлетворяет концепту reflect::Enum, и не удовлетворяет reflect::Variable - ссылка на код с проверкой.

Работа происходит с помощью "трансформаций" одних магических типов в других. Список доступных трансформаций зависит от того, каким концептам удовлетворяет исходный тип. Например, Reflection TS определяет такой шаблон, доступный только удовлетворяющим reflect::Enum магическим типам:

template <Enum T> struct get_enumerators;

// и его short-hand
template <Enum T>
using get_enumerators_t = typename get_enumerators<T>::type;

Таким образом, трансформация get_enumerators_t<MetaT> скомпилируется. С ее помощью мы получим другой магический тип, на этот раз удовлетворяющий концепту reflect::ObjectSequence.

Выведем название первого элемента enum Color спустя несколько трансформаций:

int main() {
    constexpr std::string_view name = get_name_v<get_element_t<0, get_enumerators_t<MetaT>>>;
    std::cout << "The name of the first value is \"" << name << "\"" << std::endl;
}

Ссылка на код.

Основная претензия к шаблонному подходу - неочевидность, как надо писать код. Мы хотим написать цикл по ObjectSequence? Обычным for-ом это сделать нельзя, есть только размер последовательности и получение элемента из него, и некий unpack_sequence:

template <ObjectSequence T> struct get_size;
template <size_t I, ObjectSequence T> struct get_element;
template <template <class...> class Tpl, ObjectSequence T>
  struct unpack_sequence;

Если мы хотим сделать такую элементарную задачу, как по значению enum-а получить его строковое представление, надо написать какую-то жуть, в которой совсем ничего не понятно - ссылка на код в gist.

Рефлексия в constexpr

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

Основные источники информации про текущий вариант рефлексии - документ P2320, видео-выступление Andrew Sutton на ютубе, и частично Wiki в гитхабе реализации.

Рефлексия вводится в виде оператора ^X перед рефлексируемой сущностью X. Применение оператора создаст constexpr-объект типа std::experimental::meta::info.

После манипуляций с объектом (которые должны происходить в compile-time) можно "вернуть" его в "реальный" мир через оператор [:X:] (называется "splice"). Запись [:^X:] практически эквивалентна X.

Andrew Sutton в видео приводит игрушечный пример с созданием объекта типа T****...* (количество звёздочек равно N). Вот так можно сделать через шаблоны:

template<typename T, int N>
auto make_indirect_template() {
    if constexpr (N == 0) {
        return T{};
    } else {
        return make_indirect_template<T*, N - 1>();
    }
}

А вот так можно сделать через рефлексию:

consteval meta::info make_pointer(meta::info type, int n) {
    for (int i = 0; i < n; ++i) {
        type = meta::add_pointer(type);
    }
    return type;
}

template<typename T, int N>
auto make_indirect_reflective() {
    return typename [:make_pointer(^T, N):]{};
}

Код внутри consteval-методов выполняется только в compile-time. Все consteval-методы после компиляции "испаряются", то есть их код в бинарнике отсутствует.

Можно вывести имя получившихся типов:

int main() {
    auto ptr1 = make_indirect_template<int, 42>();
    std::cout << meta::name_of(meta::type_of(^ptr1)) << std::endl;

    auto ptr2 = make_indirect_reflective<int, 42>();
    std::cout << meta::name_of(meta::type_of(^ptr2)) << std::endl;
}

Компиляция на godbolt

Соглашение о записи операторов

Записи операторов ^X и [:X:] могут не пройти проверку временем и видоизмениться к момента входа в стандарт. Но это будут взаимозаменяющие записи.

Ранее вместо ^X был reflexpr(X), вместо [:X:] был unreflexpr(X).

На данный момент текущая запись является "официальной", что можно увидеть в github-тикете про P2320.

Компиляция и запуск

Свой код с использованием рефлексии можно запустить на cppx.godbolt.com, выбрав компилятор p2320 trunk.

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

Сборка с использованием docker-образа

docker-образ был создан по этому Dockerfile, собирал ветку paper/p2320.

Образ можно загрузить:

docker pull sehnsucht88/clang-p2320

Пусть ваш исходник code.cpp находится в директории /home/username/cpp, тогда запускать можно так:

docker run --rm -v /home/username/cpp:/cpp sehnsucht88/clang-p2320 -std=c++2a -freflection -stdlib=libc++ /cpp/code.cpp -o /cpp/bin

После компиляции в директории /home/username/cpp будет лежать запускаемый бинарник bin

На случай удаления репозитория я сделал форк - https://github.com/Izaron/meta.

Рефлексия на практике

Теперь попробуем написать что-то рефлексивное.

Значение enum-а в строковом представлении

В отличие от "рефлексии на шаблонах", в "рефлексии на constexpr" это сделать намного проще. Пример кода (немного изменил код из видео Andrew Sutton):

template<typename T>
requires std::is_enum_v<T>
constexpr std::string_view to_string(T value) {
    template for (constexpr meta::info e : meta::members_of(^T)) {
        if ([:e:] == value) {
            return meta::name_of(e);
        }
    }
    throw std::runtime_error("Unknown enum value");
}

template for - это фича, которая не успела войти в стандарт C++20. В нашем случае она раскрывает range методом копипаста. Пусть у нас такой enum:

enum LightColor { Red, Green, Blue };

Тогда метод раскроется в такой вид:

template<>
constexpr std::string_view to_string<LightColor>(T value) {
    { if (Red == value) return "Red"; }
    { if (Green == value) return "Green"; }
    { if (Blue == value) return "Blue"; }
    throw std::runtime_error("Unknown enum value");
}

Аналогично можно сделать метод, который по строковому представлению вернет значение enum-а

Исходник from_string
template<typename T>
requires std::is_enum_v<T>
constexpr std::optional<T> from_string(std::string_view value) {
    template for (constexpr meta::info e : meta::members_of(^T)) {
        if (meta::name_of(e) == value) {
            return [:e:];
        }
    }
    return {};
}

Компиляция на godbolt

Проверка функций на internal linkage

Можно реализовать проверку на отсутствие видимых "снаружи" (вне единицы трансляции) методов с помощью вызова meta::is_externally_linked.

Небольшое отступление - в форке компиляции доступно несколько вспомогательных методов, работающих в compile-time:

  • __reflect_dump - принимает meta::info, выведет в терминал AST соответствующей ему сущности.

  • __compiler_error - принимает строку, завершает компиляцию ошибкой с выводом данной строки.

  • __concatenate - соединяет несколько строковых литералов в один.

Первые два метода нужны для удобства разработки compile-time кода. Третий метод нужен, потому что std::string в compile-time пока еще нет в стандарте (но когда-то будет).

Про meta::info есть один факт - в некоторых случаях мы не можем написать метод так:

consteval void foo(meta::info r) { /* ... */ }

потому что компилятор думает, что meta::info протекает в run-time... Зато можем написать так:

template<meta::info R>
consteval void foo() { /* ... */ }

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

template<meta::info R>
consteval void check_functions_linkage() {
    static_assert(meta::is_namespace(R));

    template for (constexpr meta::info e : meta::members_of(R)) {
        if constexpr (meta::is_function(e)) {
            __reflect_dump(e);
            if constexpr (meta::is_externally_linked(e)) {
                constexpr auto error_msg =
                    __concatenate("The method '", meta::name_of(e), "' is externally linked");
                __compiler_error(error_msg);
            }
        }

        if constexpr (meta::is_namespace(e)) {
            check_functions_linkage<e>();
        }
    }
}

Заведем игрушечный namespace - компиляция будет падать так, как нам нужно:

namespace outer {
    bool foo(int i) { return i == 13; }
    std::string bar(std::string s) { return s + s; };

    namespace inner {
        double fizz() { return 3.14; }
    } // namespace inner
} // namespace outer

int main() {
    check_functions_linkage<^outer>();
    std::cout << "compiled!" << std::endl;
}

Чтобы компиляция перестала падать, нужно сделать методы имеющими internal linkage.

Способы это сделать

Написать модификатор static

namespace outer {
    static bool foo(int i) { return i == 13; }
    static std::string bar(std::string s) { return s + s; };

    namespace inner {
        static double fizz() { return 3.14; }
    } // namespace inner
} // namespace outer

Или поместить методы внутри анонимного namespace

namespace outer {
namespace {
    bool foo(int i) { return i == 13; }
    std::string bar(std::string s) { return s + s; };

    namespace inner {
        double fizz() { return 3.14; }
    } // namespace inner
} // anonymous namespace
} // namespace outer

При желании можно пропарсить всё, до чего только можно дотянуться - если итерироваться по глобальному namespace (он же ::). Рефлексия глобального namespace это ^::.

Компиляция на godbolt

Проверка, что тип является интерфейсом

Можно проверить, что тип является "абстрактным", то есть имеет хотя бы один чисто виртуальный метод, через std::is_abstract.

Понятие "интерфейс" в стандарте не зафиксировано, но можно выработать для него требования:

  1. Все user-defined методы (т.е. которые юзер написал сам, а не которые сгенерированы компилятором) публичные и чисто виртуальные.

  2. У класса нет переменных.

  3. В классе есть публичный виртуальный деструктор, являющийся defaulted.

Вот как можно описать эти требования:

namespace traits {

template<typename T>
consteval bool is_interface_impl() {
    constexpr meta::info refl = ^T;
    if constexpr (meta::is_class(refl)) {
        template for (constexpr meta::info e : meta::members_of(refl)) {
            // interfaces SHALL NOT have data members
            if constexpr (meta::is_data_member(e)) {
                return false;
            }
            // every user function in interfaces SHOULD BE public and pure virtual
            if constexpr (meta::is_function(e) && !meta::is_special_member_function(e)) {
                if constexpr (!meta::is_public(e) || !meta::is_pure_virtual(e)) {
                    return false;
                }
            }
            // the destructor SHOULD BE virtual and defaulted
            if constexpr (meta::is_function(e) && meta::is_destructor(e)) {
                if constexpr (!meta::is_public(e) || !meta::is_defaulted(e) || !meta::is_virtual(e)) {
                    return false;
                }
            }
        }
        return true;
    }
    return false;
}

template<typename T>
constexpr bool is_interface = is_interface_impl<T>();

} // namespace traits

Можно протестировать написанный метод:

Разные тесты
// IS NOT abstract, IS NOT interface
class foo {
public:
    void foo_void();
private:
    int _foo_int;
};
static_assert(not std::is_abstract_v<foo>);
static_assert(not traits::is_interface<foo>);

// IS abstract, IS NOT interface
class bar {
public:
    virtual void bar_void() = 0;
    std::string bar_string();
private:
    int _foo_int;
};
static_assert(    std::is_abstract_v<bar>);
static_assert(not traits::is_interface<foo>);

// IS abstract, IS NOT interface
class fizz {
public:
    virtual void fizz_void() = 0;
    std::string fizz_string();
};
static_assert(    std::is_abstract_v<fizz>);
static_assert(not traits::is_interface<fizz>);

// IS abstract, IS NOT interface
class buzz {
public:
    virtual void buzz_void() = 0;
    virtual std::string buzz_string() = 0;
};
static_assert(    std::is_abstract_v<buzz>);
static_assert(not traits::is_interface<buzz>);

// IS abstract, IS NOT interface
class biba {
public:
    virtual ~biba() { /* ... not defaulted dtor ... */ };
    virtual void biba_void() = 0;
    virtual std::string biba_string() = 0;
};
static_assert(    std::is_abstract_v<biba>);
static_assert(not traits::is_interface<biba>);

// IS abstract, IS interface
class boba {
public:
    virtual ~boba() = default;
    virtual void boba_void() = 0;
    virtual std::string boba_string() = 0;
};
static_assert(    std::is_abstract_v<boba>);
static_assert(    traits::is_interface<boba>);

Компиляция на godbolt

Сериализация объекта в JSON

Сериализация в JSON это такой FizzBuzz для любителей рефлексии. Каждый уважающий себя разработчик рефлексии рано или поздно это напишет.

В своем видео Andrew Sutton разбирает вопрос с JSON, но больше как псевдокод. Мы напишем свою реализацию.

Если модель данных немаленькая, то с "голым" JSON работать становится очень неудобно - всё нетипизированно и как будто постоянно лезешь в свалку данных, чтобы получить нужные поля. Можно конвертировать JSON в свои структуры, но это влечет кучу копипаста - чего можно избежать при наличии рефлексии.

Базовые типы JSON это Number, String, Boolean, Array, Object; пустое значение - null. Напишем концепты для каждого типа.

Number это каждый тип, удовлетворяющий std::is_arithmetic:

template<typename T>
concept JsonNumber = std::is_arithmetic_v<T>;

String это строковой тип, причем объект должен владеть строкой, а не просто знать о ней (как std::string_view). Потому что где сериализация - там и десериализация, поэтому нужен владеющий тип. Это, конечно, только std::string:

template<typename T>
concept JsonString = std::same_as<std::string, T>;

Boolean это просто bool:

template<typename T>
concept JsonBoolean = std::same_as<bool, T>;

Array должен быть контейнером из последовательных элементов. Другими словами, это должен быть SequenceContainer - std::array/std::vector/std::deque/std::forward_list/std::list.

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

концепт JsonArray

спустя несколько ошибок компиляции...

static constexpr meta::info vector_refl = ^std::vector;
static constexpr meta::info array_refl = ^std::array;
static constexpr meta::info deque_refl = ^std::deque;
static constexpr meta::info list_refl = ^std::list;
static constexpr meta::info forward_list_refl = ^std::forward_list;

template<typename T>
consteval bool is_json_array_impl() {
    if constexpr (meta::is_specialization(^T)) {
        constexpr auto tmpl = meta::template_of(^T);
        constexpr bool result =
            tmpl == vector_refl || tmpl == array_refl ||
            tmpl == deque_refl || tmpl == list_refl ||
            tmpl == forward_list_refl;
        return result;
    }
    return false;
}

template<typename T>
concept JsonArray = is_json_array_impl<T>();

В данный момент сравнение как tmpl == ^std::vector крашит clang, поэтому придется писать так.

Object это просто класс/структура. В нем, конечно, должны быть дополнительные ограничения, чтобы сериализовать/десериализовать можно было достаточно простой тип - скажем, без членов со ссылочными типами. Но пока обойдемся базовым примером.

template<typename T>
concept JsonObject = std::is_class_v<T>;

Значение null можно ввести для std::optional, который не содержит значения.

концепт JsonNullable
static constexpr meta::info optional_refl = ^std::optional;

template<typename T>
consteval bool is_json_nullable_impl() {
    if constexpr (meta::is_specialization(^T)) {
        return meta::template_of(^T) == optional_refl;
    }
    return false;
}

template<typename T>
concept JsonNullable = is_json_nullable_impl<T>();

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

Особенность работы с концептами

В своем видео Andrew Sutton дает мега-совет - поскольку один тип может удовлетворять нескольким концептам, то не надо писать код вроде:

    template<Concept1 T>
    void write(T const& t) { /* ... */ }

    template<Concept2 T>
    void write(T const& t) { /* ... */ }

    template<Concept3 T>
    void write(T const& t) { /* ... */ }

Потому что рано или поздно можно попасть на неоднозначность выбора метода. Поэтому надо делать диспетчеризацию, проверяя концепты по приоритетности:

    template<typename T>
    void write(T const& t) {
        if constexpr (Concept1<T>) {
            write_concept1(t);
        } else if constexpr (Concept2<T>) {
            write_concept2(t);
        } else if constexpr (Concept3<T>) {
            write_concept3(t);
        }
    }

Сделаем класс json_writer, пусть он принимает объект, куда можно стримить выходной поток

template<typename Out>
class json_writer {
public:
    json_writer(Out& out)
        : _out{out}
    {}
    
    // ... другой код ...

private:
    Out& _out;
};

Реализуем метод для сериализации, который будет "диспетчером" для разных JSON-типов:

    template<typename T>
    void write(T const& t) {
        if constexpr (JsonNullable<T>) {
            write_nullable(t);
        } else if constexpr (JsonNumber<T>) {
            write_number(t);
        } else if constexpr (JsonString<T>) {
            write_string(t);
        } else if constexpr (JsonBoolean<T>) {
            write_boolean(t);
        } else if constexpr (JsonArray<T>) {
            write_array(t);
        } else if constexpr (JsonObject<T>) {
            write_object(t);
        }
    }

Методы, которые вызываются из write, могут естественным образом делать рекурсивный запрос в write снова. Реализуем запись nullable-типа:

    template<JsonNullable T>
    void write_nullable(T const& t) {
        if (t.has_value()) {
            write(*t);
        } else {
            _out << "null";
        }
    }

Записи числового, строкового, булевого типов нерекурсивны:

    template<JsonNumber T>
    void write_number(const T t) {
        _out << t;
    }

    template<JsonString T>
    void write_string(T const& t) {
        _out << '"' << t << '"';
    }

    template<JsonBoolean T>
    void write_boolean(const T t) {
        if (t) {
            _out << "true";
        } else {
            _out << "false";
        }
    }

Запись массива достаточно проста - надо только правильно ставить запятые, разделяющие объекты:

    template<JsonArray T>
    void write_array(T const& t) {
        _out << '[';
        bool is_first_item = true;
        for (const auto& item : t) {
            if (is_first_item) {
                is_first_item = false;
            } else {
                _out << ',';
            }
            write(item);
        }
        _out << ']';
    }

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

    template<JsonObject T>
    void write_object(T const& t) {
        _out << '{';
        bool is_first_member = true;

        template for (constexpr meta::info e : meta::members_of(^T)) {
            if constexpr (meta::is_data_member([:^e:])) {
                if (is_first_member) {
                    is_first_member = false;
                } else {
                    _out << ',';
                }

                _out << '"' << meta::name_of(e) << '"';
                _out << ':';
                write(t.[:e:]);
            }
        }

        _out << '}';
    }

Создадим модель данных - пусть это будет библиотека, у которой несколько книг, один адрес, и опционально "описание"

namespace model {

struct book {
    std::string name;
    std::string author;
    int year;
};

struct latlon {
    double lat;
    double lon;
};

struct library {
    std::vector<book> books;
    std::optional<std::string> description;
    latlon address;
};

} // namespace model

Зададим библиотеке адрес, добавим несколько книг, и выведем ее в формате JSON:

int main() {
    model::library l;
    l.address = model::latlon{.lat = 51.507351, .lon = -0.127696};
    l.books.push_back(model::book{
        .name = "The Picture of Dorian Gray",
        .author = "Oscar Wilde",
        .year = 1890,
    });
    l.books.push_back(model::book{
        .name = "Fahrenheit 451",
        .author = "Ray Bradbury",
        .year = 1953,
    });
    l.books.push_back(model::book{
        .name = "Roadside Picnic",
        .author = "Arkady and Boris Strugatsky",
        .year = 1972,
    });

    json::json_writer{std::cout}.write(l);
    std::cout << std::endl;
}

Программа выведет неотформатированный JSON:

{"books":[{"name":"The Picture of Dorian Gray","author":"Oscar Wilde","year":1890},{"name":"Fahrenheit 451","author":"Ray Bradbury","year":1953},{"name":"Roadside Picnic","author":"Arkady and Boris Strugatsky","year":1972}],"description":null,"address":{"lat":51.5074,"lon":-0.127696}}
Отформатированный вид такой:
{
    "books": [
        {
            "name": "The Picture of Dorian Gray",
            "author": "Oscar Wilde",
            "year": 1890
        },
        {
            "name": "Fahrenheit 451",
            "author": "Ray Bradbury",
            "year": 1953
        },
        {
            "name": "Roadside Picnic",
            "author": "Arkady and Boris Strugatsky",
            "year": 1972
        }
    ],
    "description": null,
    "address": {
        "lat": 51.5074,
        "lon": -0.127696
    }
}

Компиляция на godbolt

Если бы сериализацию/десериализацию надо было сделать в реальном проекте, я бы посоветовал добавить "прокладку" в виде существующей json-библиотеки, например nlohmann/json.

То есть мы бы переводили объект "нашей" структуры в объект из json-библиотеки, а этот объект уже конвертировался бы в строку. При десериализации наоборот - строка в json-объект, json-объект в "наш" объект.

Это нужно, чтобы не переизобретать велосипед - с "прокладкой" работать проще и надежнее, чем самому что-то парсить.

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

Универсальный метод сравнения двух объектов

Возьмем model::book из предыдущего кода. Если мы попытаемся сравнить два объекта этого типа, то получим ошибку компиляции

    model::book a, b;
    std::cout << (a == b) << std::endl; // тут ошибка компиляции

Можно выработать свои правила для универсального сравнения:

  1. Если объекты можно сравнить, то есть вызов a == b скомпилируется, то результат сравнения - вызов этого оператора.

  2. Если объект - итерируемый контейнер (как std::vector), то проверим, что размеры совпадают, и сравним каждый элемент контейнера.

  3. Иначе проитерируемся по членам типа и сравним каждый член отдельно.

Для первого и второго пункта концепты пришлось написать самому, так как существующие не нашел...

namespace bicycle {

template <class T>
constexpr bool equality_comparable = requires(const T& a, const T& b) {
    std::is_convertible_v<decltype(a == b), bool>;
};

template <class T>
constexpr bool iterable = requires(const T& t, size_t i) {
    t[i];
    std::begin(t);
    std::end(t);
    std::size(t);
};

} // namespace bicycle

Теперь напишем наш метод, как и планировали - с проверкой с первого по третий пункт? На самом деле нет - первый и второй пункт надо поменять местами

Концепты иногда работают не так, как ожидали

Если проверить первый концепт, то можно обнаружить подставу:

static_assert(    bicycle::equality_comparable<int>);
static_assert(    bicycle::equality_comparable<std::string>);
static_assert(    bicycle::equality_comparable<std::optional<std::string>>);
static_assert(    bicycle::equality_comparable<std::vector<model::book>>); // <<< :(
static_assert(not bicycle::equality_comparable<model::book>);
static_assert(not bicycle::equality_comparable<model::library>);

Сравнение двух объектов типа model::book не скомпилируется, так же, как типа std::vector<model::book>. Но концепт резольвится в true!

Дело в том, что концепт смотрит на сигнатуру метода, а не на весь метод. Он видит, что у вектора оператор сравнения объявлен:

template< class T, class Alloc >
bool operator==( const std::vector<T,Alloc>& lhs,
                 const std::vector<T,Alloc>& rhs );

А в определение метода он не лезет, к тому же это может быть невозможно - определение может лежать в другом translation unit. То, что в итоге код не скомпилируется, для концепта это "уже не его проблемы".

Напишем наш метод:

namespace equal_util {

template<typename T>
bool equal(const T& a, const T& b) {
    if constexpr (bicycle::iterable<T>) {
        if (a.size() != b.size()) {
            return false;
        }
        for (size_t i = 0; i < a.size(); ++i) {
            if (!equal(a[i], b[i]))
                return false;
        }
        return true;
    } else if constexpr (bicycle::equality_comparable<T>) {
        return a == b;
    } else {
        template for (constexpr meta::info e : meta::members_of(^T)) {
            if constexpr (meta::is_data_member([:^e:])) {
                if (!equal(a.[:e:], b.[:e:])) {
                    return false;
                }
            }
        }
        return true;
    }
}

} // namespace equal_util

Возможно, стоило бы для типов float и double сравнивать их разницу с эпсилоном... Но пока обойдемся без этих подарочков.

Проверим метод - в первый раз выведется true, во второй раз false, успех!

int main() {
    model::library a, b;

    a.address = model::latlon{.lat = 51.507351, .lon = -0.127696};
    b.address = a.address;

    a.books.push_back(model::book{
        .name = "The Picture of Dorian Gray",
        .author = "Oscar Wilde",
        .year = 1890,
    });
    b.books = a.books;

    std::cout << std::boolalpha;
    std::cout << equal_util::equal(a, b) << std::endl;
    b.books.clear();
    std::cout << equal_util::equal(a, b) << std::endl;
}

Компиляция на godbolt

Контейнер Dependency Injection

И наконец, мы сделаем собственный контейнер для Dependency Injection!

Этот паттерн программирования хардкорно используется, например, в Spring - самом популярном Java-фреймворке.

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

Смысл паттерна в том, что вместо того, чтобы компонент сам создавал зависимые компоненты, эти компоненты создавал бы фреймворк. И потом давал бы их компоненту через конструктор (все компоненты сразу) либо через сеттер-методы (по одному сеттер-методу на компонент).

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

Создадим модель управления для сервиса а-ля "URL Shortener", который принимает длинные ссылки и отдает короткие (и наоборот). У нас будет, очень условно, четыре компонента (в реальности было бы побольше):

  1. s3_storage - сервис, который умеет брать картинку из s3-хранилища и возвращать ее.

  2. database - сервис-"прокладка" для работы с базой данных

  3. link_shortener - сервис, принимающий длинную ссылку и возвращающий короткую (и наоборот). Зависит от database, где хранит соответствие между ссылками.

  4. http_server - сервис, обрабатывающие запросы по http. Зависит от s3_storage (показ лого на сайте), link_shortener (понятно для чего), database (куда пишет всякую статистику про посетителя сайта).

Зависимости в программе
Зависимости в программе

Опишем компоненты в коде:

namespace component {

class database {
public:
    void post_construct() {
        /* тут инициализируем подключение к БД */
    }

    /* тут некие методы об операциях в БД */
};

class link_shortener {
public:
    void set_component(std::shared_ptr<database> component) {
        _database = std::move(component);
    }

    /* тут некие методы link_shortener. */ 
    /* метод post_construct() не нужен */

private:
    std::shared_ptr<database> _database;
};

class s3_storage {
public:
    /* тут некие методы s3_storage. */ 
    /* метод post_construct() не нужен */
};

class http_server {
public:
    void set_component(std::shared_ptr<s3_storage> component) {
        _s3_storage = std::move(component);
    }

    void set_component(std::shared_ptr<link_shortener> component) {
        _link_shortener = std::move(component);
    }

    void set_component(std::shared_ptr<database> component) {
        _database = std::move(component);
    }

    void post_construct() {
        /* тут поднимаем http-сервер и ждём запросы */
    }

private:
    std::shared_ptr<s3_storage> _s3_storage;
    std::shared_ptr<link_shortener> _link_shortener;
    std::shared_ptr<database> _database;
};

} // namespace component

Что должен сделать фреймворк:

  1. Создать компоненты через std::make_shared, каждый компонент должен быть создан ровно один раз.

  2. Вызвать set_component с готовыми зависимыми компонентами.

  3. Когда все нужные set_component вызваны, вызвать метод post_construct, если он есть в классе. Сначала вызывается у зависимых компонент, потом у зависящих.

  4. Когда "корневой компонент" (в нашем случае http_server) закончит работу post_construct, в правильном порядке уничтожить компоненты, чтобы на момент вызова деструктора все зависимые компоненты были "живы".

Создадим заготовку класса:

namespace dependency_injection {

static constexpr meta::info shared_ptr_refl = ^std::shared_ptr;

class components_builder {
public:
    template<typename Component>
    std::shared_ptr<Component> build() && {
        return build_component_impl<Component>();
    }

private:
    using ready_components_container = std::unordered_map<std::string_view, std::any>;
    static constexpr std::string_view COMPONENT_INJECTION_FUNCTION_NAME = "set_component";
    static constexpr std::string_view COMPONENT_POST_CONSTRUCT_FUNCTION_NAME = "post_construct";

private:
    // другие методы...
  
private:
    ready_components_container _ready_components;
};

} // namespace dependency_injection

Готовые компоненты хранятся в хешмапе. Значения хешмапы имеют тип std::any, потому что компоненты не имеют общего типа.

Создадим метод-"прокладку", который сначала ищет компонент в хешмапе, а если не найдет, то строит компонент:

    // don't build component again if already has one built
    template<typename Component>
    std::shared_ptr<Component> build_or_get_component() {
        std::shared_ptr<Component> component;

        constexpr std::string_view comp_name = meta::name_of(meta::entity_of(^Component));
        if (auto _ready_iter = _ready_components.find(comp_name); _ready_iter != _ready_components.end()) {
            component = std::any_cast<decltype(component)>(_ready_iter->second);
        } else {
            component = build_component_impl<Component>();
            _ready_components[comp_name] = component;
        }

        return component;
    }

Чтобы построить компонент, надо создать его объект через std::make_shared, потом построить все зависимые компоненты и вызвать для каждого set_component, потом вызвать метод post_construct при его наличии.

    template<typename Component>
    std::shared_ptr<Component> build_component_impl() {
        auto component = std::make_shared<Component>();
        build_dependent_components(*component);
        try_call_post_construct(*component);
        return component;
    }

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

    template<meta::info R>
    static constexpr bool is_callable_function(std::string_view expected_function_name) {
        // drop special functions and non-public functions
        if constexpr (meta::is_function(R) && meta::is_public(R) && !meta::is_special_member_function(R)) {
            constexpr std::string_view function_name = meta::name_of(R);
            return function_name == expected_function_name;
        }
        return false;
    }

Как мы можем определить зависимые компоненты:

  1. Ищем все методы с названием set_component. Пусть мы зафиксировали один такой метод.

  2. Проверяем, что в этом методе ровно один параметр.

  3. Тип этого параметра должен являться специализацией шаблона std::shared_ptr.

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

  5. Вызываем set_component с компонентом из п. 4.

С этим планом сделаем метод build_dependent_components:

    template<typename Component>
    void build_dependent_components(Component& component) {
        template for (constexpr meta::info e : meta::members_of(^Component)) {
            // iterate through functions
            if constexpr (is_callable_function<e>(COMPONENT_INJECTION_FUNCTION_NAME)) {
                // the function should have exactly one parameter
                constexpr auto param_range = meta::parameters_of(e);
                static_assert(size(param_range) == 1, "Please pass only one parameter");
                constexpr meta::info param = *param_range.begin();

                // the type of the parameter should be std::shared_ptr<U>
                constexpr meta::info param_type = meta::type_of(param);
                static_assert(meta::is_specialization(param_type), "Please pass std::shared_ptr<component>");
                static_assert(meta::template_of(param_type) == shared_ptr_refl, "Please pass std::shared_ptr<component>");

                // obtain dependent component type
                using SharedPtrType = typename [:param_type:];
                using DependentComponentType = typename SharedPtrType::element_type;

                // build the dependent component (if not built yet) and give it to the original component
                auto dependent_component = build_or_get_component<DependentComponentType>();
                component.[:e:](dependent_component);
            }
        }
    }

Вызов post_construct выглядит проще:

    template<typename Component>
    void try_call_post_construct(Component& component) {
        template for (constexpr meta::info e : meta::members_of(^Component)) {
            if constexpr (is_callable_function<e>(COMPONENT_POST_CONSTRUCT_FUNCTION_NAME)) {
                constexpr auto param_range = meta::parameters_of(e);
                static_assert(size(param_range) == 0, "Please don't pass parameters in \"post_construct\"");
                component.[:e:]();
            }
        }
    }

Осталось только установить "корневой компонент" и запустить весь процесс:

int main() {
    dependency_injection::components_builder().build<component::http_server>();
}

Если для каждого компонента добавить лог имени вызываемого метода в конструкторе, деструкторе, set_component и post_construct, то можно увидеть, что именно делает фреймворк:

call "http_server::http_server()"
call "s3_storage::s3_storage()"
call "s3_storage::post_construct()" <<<<<<<<<< THE COMPONENT IS READY
call "http_server::set_component(std::shared_ptr<s3_storage>)"
call "link_shortener::link_shortener()"
call "database::database()"
call "database::post_construct()" <<<<<<<<<< THE COMPONENT IS READY
call "link_shortener::set_component(std::shared_ptr<database>)"
call "link_shortener::post_construct()" <<<<<<<<<< THE COMPONENT IS READY
call "http_server::set_component(std::shared_ptr<link_shortener>)"
call "http_server::set_component(std::shared_ptr<database>)"
call "http_server::post_construct()" <<<<<<<<<< THE COMPONENT IS READY
call "http_server::~http_server()"
call "link_shortener::~link_shortener()"
call "database::~database()"
call "s3_storage::~s3_storage()"

Фреймворк все делает правильно!

Из того, что можно добавить:

  1. Проверку на циклы зависимостей - их быть не должно. Кажется, циклы возможно обнаружить в compile-time.

  2. Можно зависеть от интерфейса, а не от реализации, "как в лучших домах Парижу".

Зависимость от интерфейса, а не от реализации

Сервис s3_storage - это просто реализация сервиса по работе с хранилищем картинок.

Можно сделать так, чтобы s3_storage наследовался от интерфейса image_storage, и в http_server был бы метод set_component(std::shared_ptr<image_storage>).

Рефлексия могла бы распарсить весь namespace, найти реализацию интерфейса, и создать его.

Другие примеры рефлексивного программирования

Кроме примеров выше, я сделал hasattr.cpp - имитация методов hasattr и getattr из языка Python, а также opts.cpp - типизированный парсер командной строки.

Разбирать их я не стал, потому что новой информации там нет.

Все примеры доступны на github - ссылка.

Что хочется иметь от рефлексии в будущем?

Часть методов (например, строковое представление значения enum) нужно иметь в стандартной библиотеке, чтобы не писать велосипеды.

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

Когда рефлексия войдет в C++ - пока точно не известно, но вероятнее всего, успеют к стандарту C++26.

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


  1. oxx
    04.01.2022 20:49

    Забыли самую старую и продуманую рефлексию - в Object Pascal (Delphi RTTI). Она появилась еще даже до появления Java, не говоря уже о Питоне. Очень многие вещи пошли оттуда.


  1. Furax
    05.01.2022 00:52

    Спасибо, очень интересная статья, есть о чём подумать.

    Такой вопрос. Нам в рабочем проекте приходится в автоматическом режиме перегонять код C# в C++, рефлексию приходится генерировать с нуля, но это довольно плохо работает - например, нет способа проинстанциировать шаблонный тип в компайл-тайме на основе данных рефлексии. Допустим, следующий код не имеет шансов быть переведённым на C++ (хотя бы потому, что в качестве типа переменной list не понятно, что использоать, - не void* же):

    using System;
    using System.Collections.Generic;
    using System.Reflection;
    
    class Program
    {
      static void Main(string[] args)
      {
        var boolListType = typeof(List<bool>);
    
        Console.Write("Enter System type name: ");
        string argument = Console.ReadLine();
        var argumentType = Type.GetType("System." + argument);
    
        var genericList = boolListType.GetGenericTypeDefinition();
        var requiredListType = genericList.MakeGenericType(argumentType);
        var list = Activator.CreateInstance(requiredListType);
        Console.WriteLine("Created instance of " + requiredListType.FullName);
    
        if (list is List<string> ls)
          Console.WriteLine("list is List<string>");
        else if (list is List<int> li)
          Console.WriteLine("list is List<int>");
        else if (list is List<Type> lt)
          Console.WriteLine("list is List<Type>");
        else
          Console.WriteLine("list is List<Something>");
      }
    }

    Я правильно понимаю, что через описанную в статье красоту нам в ближайшие дцать лет всё равно ничего такого не светит? Или будет какая-то подсистема, позволяющая не только анализировать типы в компайл-тайме, но и буквально составлять новые типы в рантайме?


    1. Izaron Автор
      05.01.2022 01:36
      +2

      В C++ типы всех значений вычисляются в процессе компиляции и жестко фиксированы. "Тип" auto это просто синтаксический сахар, все равно это будет какой-то реальный фиксированный тип.

      Все переменные/объекты занимают какое-то количество байт на стеке, и это количество нужно знать заранее, иначе, например, непонятно как вычислить размер stack frame у функции (ассемблеру нужно это знать).

      Вот такого в C++ не будет скорее всего никогда:

      auto list = create_list();
      // ^^^ тип list вычислится в РАНТАЙМЕ,
      // будет std::vector<int>/std::vector<std::string>/другой

      Аналог того, что происходит в C#/Java, на C++ выглядит скорее как использование std::any, который аллоцирует нужное количество байт для объекта в куче (в отличие от стека, там необязательно знать объем выделяемой памяти в compile-time). Можно иметь один из типов в рантайме:

      std::any create_list(int n) {
          if (n == 0) {
              return std::vector<int>();
          } else if (n == 1) {
              return std::vector<std::string>();
          } else {
              return std::vector<char>();
          }
      }
      
      int main() {
          int n;
          std::cin >> n;
      
          auto list = create_list(n);
      
          if (list.type() == typeid(std::vector<int>)) {
              std::cout << "got vector of int" << std::endl;
          } else if (list.type() == typeid(std::vector<std::string>)) {
              std::cout << "got vector of string" << std::endl;
          } else if (list.type() == typeid(std::vector<char>)) {
              std::cout << "got vector of char" << std::endl;
          }
      }


      1. Furax
        05.01.2022 01:43

        Спасибо. Вот я тоже подумал, что подобное вряд ли возможно, но мечтать ведь не вредно =)


        1. horror_x
          05.01.2022 04:41

          Почему вряд ли возможно? Это же прямая рабочая аналогия, тип динамически задаётся в рантайме (то что под капотом там не рефлексия по сути не имеет значения, работает ведь одинаково).

          Например, в Qt есть тип QVariant, который по сути удобная обёртка для таких вот вещей. И это отлично работает.


          1. Furax
            05.01.2022 04:55
            +1

            то что под капотом там не рефлексия по сути не имеет значения, работает ведь одинаково

            Не одинаково. QVariant может "переварить" только те типы, которые были известны на момент его компиляции (а C++ вообще - на момент компиляции хоть какого-либо модуля). Мой же пример на C# "составляет" нужный тип уже в рантайме, подставляя аргументы в обобщённый тип. Например, если пользователь введёт слово DateTime, то будет создан экземпляр List<DateTime>, и с его членами можно будет работать (через ту же рефлексию), несмотря на то, что в компилируемом коде такая параметризация дженерика List нигде не упоминалась. В C++ же нельзя в рантайме создать экземпляр vector<T>, где T получен по имени в момент выполнения, а в момент компиляции в качестве аргумента vector не использовался.

            Ситуация с вводом из консоли может показаться надуманной, но ведь имя типа может быть прочитано, к примеру, из XMLки, причём число комбинаций очень быстро выходит за тот уровень, который можно предусмотреть в коде: если vector<QDateTime> ещё можно как-то предвидеть, то какой-нибудь unordered_map<MyEnum, vector<list<map<string, set<int>>>>> - нет. Особенно, если код десериализации ничего не знает ни про какой MyEnum.


            1. horror_x
              05.01.2022 05:17
              +1

              Значит я неверно понял задачу. Конечно, шаблонный тип на лету не составишь, но в таком случае не спасёт и рефлексия. Т.е. это вовсе не проблема отсутствия рефлексии, для C++ единственный вариант это заранее инстанцировать все возможные варианты шаблонов.

              к примеру, из XMLки, причём число комбинаций очень быстро выходит за тот уровень
              Вообще слабо могу себе представить кейс, когда любой тип создаётся по внешнему вводу без ограничений. Это же как минимум небезопасно.


            1. mayorovp
              05.01.2022 11:25
              +1

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


              Если в какой-то момент нужно, к примеру, сохранить в поле кусок разметки — никто не создаёт новые неизвестные типы динамически, в том же C# используется обычный XmlElement или XElement.


              1. Furax
                05.01.2022 13:44

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

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


                1. horror_x
                  05.01.2022 18:27

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

                  Шаблоны в C++ — это заготовки кода, который генерируется только при инстанциации этого шаблона. Этот код учитывает размеры и расположение в памяти, а так же прочие особенности этих типов вроде конструкторов, деструкторов, операторов и т.п.

                  Шаблоны в C# — это обобщение типов. Это не требует заранее генерировать код на каждый возможный вариант, т.к. в generic-виде они все обрабатываются общим механизмом в рантайме.

                  Если бы в C# были шаблоны как в C++, никакая рефлексия бы не помогла. И, в то же время, ничего не мешает в C++ поверх языка реализовать такой же механизм обобщения типов, как в C#.


                  1. Furax
                    05.01.2022 19:15

                    Спасибо, что пояснили. Да, с этим согласен.


  1. Kelbon
    05.01.2022 12:29
    +1

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

    Статическая рефлексия в том виде, что её предлагают тоже избыточна, пример с узнаваниями названий из enum, такая библиотека уже есть и написана она на обычном С++ без всякой рефлексии, называется magic enum.
    Насчёт сериализации - таки тоже уже есть, вот пример библиотеки для бинарной сериализации, где описанием формата является тип из С++ единственное что нужно написать программисту чтобы его тип стал поддерживаться - using enable_magic = std::true_type;
    Плюс всяческие алгоритмы работы с таким типом как с туплом и общие алгоритмы типа tuple cat / construct from tuple и прочие
    https://github.com/kelbon/UndefinedBehavior_GoldEdition/blob/master/include/kelbon_serialization.ixx


    1. zstas
      05.01.2022 13:08
      +1

      самый актуальный пример с json. существующие библиотеки предлагают либо писать бойлерплейт самому, либо дают автогенерацию через макрос (nlohmann/json).


    1. Izaron Автор
      05.01.2022 15:20
      +2

      Да, язык хорошо живет без рефлексии много лет, но новый функционал это новые возможности.

      Можно подсмотреть, что крутого есть в других языках. Фреймворк Spring в Java в этом плане очень мощно развит.

      Так выглядит обработчик http-запросов:

      в Java
      @RestController
      class EmployeeController {
      
        private final EmployeeRepository repository;
      
        EmployeeController(EmployeeRepository repository) {
          this.repository = repository;
        }
      
        @GetMapping("/employees")
        List<Employee> all() {
          return repository.findAll();
        }
      
        @PostMapping("/employees")
        Employee newEmployee(@RequestBody Employee newEmployee) {
          return repository.save(newEmployee);
        }
      
        @GetMapping("/employees/{id}")
        Employee one(@PathVariable Long id) {
          return repository.findById(id)
            .orElseThrow(() -> new EmployeeNotFoundException(id));
        }
      
        @DeleteMapping("/employees/{id}")
        void deleteEmployee(@PathVariable Long id) {
          repository.deleteById(id);
        }
      }

      Примерно так можно бы сделать в C++, когда доработают поддержку пользовательских атрибутов:

      в C++
      class [[rest_controller]] employee_controller {
      public:
          void set_component(std::shared_ptr<employee_repository> repository) {
              _repository = std::move(repository);
          }
      
          [[get_mapping("/employees")]]
          std::vector<employee> all() {
              return _repository.find_all();
          }
      
          [[post_mapping("/employees")]]
          employee new_employee([[request_body]] employee new_employee) {
              return _repository.save(std::move(new_employee));
          }
      
          [[get_mapping("/employees/{id}")]]
          employee all([[path_variable]] size_t id) {
              std::optional<employee> e = _repository.find_by_id(id);
              if (e.has_value()) {
                  return std::move(*e);
              }
              throw employee_not_found_exception(id);
          }
      
          [[delete_mapping("/employees/{id}")]]
          void delete_employee([[path_variable]] size_t id) {
              _repository.delete_by_id(id);
          }
      
      private:
          std::shared_ptr<employee_repository> _repository;
      };


      1. Kelbon
        05.01.2022 15:39
        -2

        и что это за магические слова? Что они должны означать? Зачем это?


        1. qw1
          07.01.2022 12:31
          +1

          Задача: сделать фреймворк, на котором удобно писать HTTP REST сервер.

          Нужно, чтобы пользователь написал минимум кода, и при получении GET HTTP-запроса
          host.com/employees/17
          фрейморк бы вызвал метод
          employee find(size_t id); со значением id=17
          результат бы сериализовал в json и отдал клиенту

          а при получении POST HTTP-запроса
          host.com/employees
          с телом json: {name: "john", age: 17}
          вызвал бы метод
          employee create_new_employee(employee new_employee);
          и параметр new_employee создал бы из json-тела запроса.

          Связывание входного URL с методом и параметрами должно происходить декларативно и с минимумом бойлерплейта. В C# и java эта задача решена, в C++ — нет.

          типичный маппинг параметров на URL может быть таким, что URL
          somedb.com/region-ru/discounts/?sort=date&page=6
          преобразуется в вызов

          std::vector<item> find_discounts(size_t start_page = 0, size_t count = 0, Sort sort = Sort::none);

          с парамерами start_page = 6, sort = Sort::date, count = 0 (последнего нет в URL, остаётся значение по умолчанию).


    1. horror_x
      05.01.2022 18:01
      +4

      на обычном С++ без всякой рефлексии
      На костылях и особенностях компиляторов.
      template <typename E>
      constexpr auto n() noexcept {
        static_assert(is_enum_v<E>, "magic_enum::detail::n requires enum type.");
      #if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED
      #  if defined(__clang__)
        constexpr string_view name{__PRETTY_FUNCTION__ + 34, sizeof(__PRETTY_FUNCTION__) - 36};
      #  elif defined(__GNUC__)
        constexpr string_view name{__PRETTY_FUNCTION__ + 49, sizeof(__PRETTY_FUNCTION__) - 51};
      #  elif defined(_MSC_VER)
        constexpr string_view name{__FUNCSIG__ + 40, sizeof(__FUNCSIG__) - 57};
      #  endif
        return static_string<name.size()>{name};
      #else
        return string_view{}; // Unsupported compiler.
      #endif
      }


  1. PokimonZerg
    07.01.2022 13:23

    Статья интересная. Спасибо.

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


  1. X-Ray_3D
    07.01.2022 13:23

    Есть "забавный" самописный проект с++20 компилятора с рефлексией https://www.circle-lang.org/

    По чему в комтете отвергают '@' ведь это ни с чем не спутаешь и сразу понятно о чём речь.

    Фронту парсить @ проще чем нагромождение [:^:], я думаю это гораздо эффективне. А то всё это напоминает триграфы которые по моему ни кто не использовал и ни разу не видел.