Comments 63
Чувак захардкодил данные в исполняемый файл и почему-то считает, что это круто.
Хотя исходно смысл рефлексии в программировании прямо противоположный - менять код при выполнении программы.
Как сказать, что ты программируешь только на какой-нибудь Java, не говоря об этом.
Кто виноват, что у вас такой плохой язык, что он не в состоянии такое сделать на компиляции? что у вас ошибки рефлексии можно увидеть ТОЛЬКО на рантайме
На Java гнать волну не стоит.
Смысл хардкодить из json во время компиляции? Проще тогда сразу средствами языка всё описать и в include подключить.
А рефлексия во время выполнения это средство расширения функционала без переписывания кода.
Кто тебе сказал, что это нельзя применить и в рантайме? Средство расширение функционала - какого? 99% кода рефлексии на Java делает тупо обход всех полей и сериализацию
Вы просто реально достали, у вас ущербное мышление, что динамическая рефлексия - это вся рефлексия, которая возможна, хотя динамическую рефлексию при желании можно реализовать через статическую, но не наоборот.
Представьте, что ваш json меняется пользователем в произвольные моменты времени. Попробуйте это реализовать через "статическую рефлексию".
Это вы тут реально достали своим ущербным мышлением в парадигме статической рефлексии.
Кто тебе сказал, что это нельзя применить и в рантайме?
Там всё в consteval'ах.
Смысл хардкодить из json во время компиляции? Проще тогда сразу средствами языка всё описать и в include подключить.
А если этот json производится какой-то внешней тулзиной, к исходникам которой у вас даже доступа нет?
Откуда тогда уверенность, что он не изменится за время эксплуатации вашей программы?
Вот у нас ровно так и делается. Есть программа на Java и программа на Qt. И нужно создать общий так сказать header file из которого обе программы будут брать актуальный для данного релиза поток данных. Динамически подгружать - только ошибки плодить. А синтаксис у языков разный, JSON бы спас
Перекомпилировать программу из-за изменения данных – жуткий костыль. Так-то ничего в аргументации по поводу хардкодинга не изменилось за последние 70 лет. И именно для того, чтобы не нужно было зашивать структуру данных в исполняемый модуль, но в то же время необязательно было бы писать настоящий самомодифицирующийся код (что возможно далеко не во всех языках) и была придумана рефлексия.
А постоянно менять протоколы обмена данных без обеспечения обратной совместимости это просто архитектурная безалаберность.
Так обратной совместимости не будет как раз в том случае, когда вы в код забиваете конкретную версию протокола при компиляции.
А в рефлексии откуда она новая возьмется? Или сам json должен передавать логику своей обработки?
Но тогда это remote code execution vulnerability.
Даже если новая не возьмется, то старая не пропадет при реализации новой.
А почему у меня при реализации на типах должна старая пропасть? Я же сам пишу как надо, а для всяких сложных штук придумали типы-суммы с семантикой oneof. Такое и плюсы тоже поддерживают, хотя и не очень удобно.
Если делать так, как показано в статье, то частью кода программы автоматически станет преобразованная форма именно текущей версии интерфейсного файла на момент компиляции. А при традиционной рефлексии код можно было бы выбирать по версии файла на момент исполнения.
Отдельно добавлю про remote code execution vulnerability, про что не дописал вчера. Рассмотрим программу-компилятор. Вот уж remote code execution vulnerability какая здоровенная в нём, не правда ли? Или программа-терминал. Это я к тому, что remote code execution vulnerability подразумевает недружественное окружение для этого code, а не просто всякий случай исполнения пришедших со стороны инструкций. То есть всё зависит от задачи.
При зашивании всего в программу, можно на этапе компиляции проверить данные вместо падения в рантайме. Плюс не надо писать обработку ошибок. Плюс не надо тратить ресурсы на парсинг при каждом запуске. Плюс не везде есть файловая система в привычном виде, чтобы из нее читать конфиг (embedded, wasm)
Нет гарантии что статическая рефлексия защищает от ошибок.
Во-первых, какой такой программы. В статье описан потенциальный механизм, который пока не реализуем из-за отсутствия полноценных compile-time парсеров (хоть JSON-а, хоть еще чего-нибудь). Т.е., с моей точки зрения, это нельзя рассматривать как готовый "рецепт", а просто как proof-of-concept для того, чтобы разработчики могли осознать мощь новых возможностей.
Во-вторых, это в данном proof-of-concept из json-а генерируются объекты с конкретными значениями, что и дает вам возможность доколупаться до воображаемого вами хардкодинга. Но таким же образом могут генерироваться и структуры по внешним спецификациям (наприимер, из .proto-файлов или из описаний Swagger-а). И тогда содержимое внешних файлов спецификаций не может просто так поменяться. А если меняется, то под новую спецификацию и логику нужно будет подправить.
Так что нет, фатальных проблем с "хардкодингом" я лично в тексте не вижу.
Сам по себе принцип макрогенерации кода я ни в коем случае не ставлю под сомнение (хотя реализован он в C++ настолько криво, что пользоваться им в проде вряд ли кто-то решится, как верно замечено в комментарии ниже). Но конкретный пример даже в качестве proof-of-concept, с моей точки зрения, не имеет смысла. Это чем-то напоминает пресловутое объяснение ООП на примере наследования животных друг от друга.
Вполне возможно, году к 2030 в компилятор C++ вкорячат полноценный парсер и генератор AST, который сможет делать почти всё как в Лиспе за 70 лет до того. Только не забыть бы в процессе, зачем вообще это было нужно. А вроде уже забыли, судя по таким примерам.
Но конкретный пример даже в качестве proof-of-concept, с моей точки зрения, не имеет смысла.
Ваше мнение очень важно для всех нас, продолжайте держать нас в курсе.
всё как в Лиспе за 70 лет до того
Лиспу это очень сильно помогло. Такой востребованный в последние 30 лет язык, что прям образец для подражания.
Только не забыть бы в процессе, зачем вообще это было нужно.
И зачем же?
И зачем же?
Ну вот я и спрашиваю, зачем.
С ваших слов, это
потенциальный механизм, который пока не реализуем
***
Лиспу это очень сильно помогло. Такой востребованный в последние 30 лет язык, что прям образец для подражания.
Любите востребованность – пишите на Питоне. Там, кстати, и с рефлексией получше.
Ну вот я и спрашиваю, зачем.
Простите, но я вот в этой вашей фразе "Вполне возможно, году к 2030 в компилятор C++ вкорячат полноценный парсер и генератор AST, который сможет делать почти всё как в Лиспе за 70 лет до того. Только не забыть бы в процессе, зачем вообще это было нужно. А вроде уже забыли, судя по таким примерам." вопроса не увидел.
Так вы знаете зачем нужна рефлексия или у меня спрашиваете?
С ваших слов, это
С моих слов это не это.
Статья показывает как взять некое описание и из этого описания сгенерировать набор C++ных структур + набор объектов с конкретными значениями. Что может быть использовано для ряда задач из категории eDSL. Например, генерация эффективных разборщиков регулярных выражений.
Или генераторов лексических/грамматических парсеров. Например, нам пришлось сделать свой парсер на базе PEG. Только для этого пришлось упороться шаблонами, что привело, как минимум, к двум неприятным последствиям:
Трудно это дело отлаживать.
Даже для небольших грамматик создаются такие длинные имена типов, что в выхлопе компилятора при ошибке одно имя может занимать целый экран. И есть большие сомнения, что компилятор в принципе осилит грамматики размером побольше.
В то же время, если в 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>);
Любите востребованность – пишите на Питоне.
Не говорите людям что им делать и им не придется говорить вам куда отправиться.
Зачем нужна рефлексия, вы можете прочитать в интернете, набрав что-нибудь вроде "рефлексия в программировании". А именно, для того, чтобы программа могла исследовать и менять свою собственную структуру на этапе выполнения, приспосабливая её к своим входным данным. А "рефлексия на этапе компиляции" – это просто оксюморон. Для компилятора сам текст программы является входными данными.
Не надо было авторам весьма ограниченный по своим возможностям (да и вообще хоть какой) макропроцессор называть "рефлексией", не было б споров.
Воообще, хоть в философии, хоть в психологии, хоть в программировании слово "рефлексия" обозначает по сути одно и то же – действия или мысли отражаются (reflect) в каком-то зеркале, и это отражение служит основой для новых действий или мыслей, замыкая цикл обратной связи. В компиляции же никакого отражения и цикла обратной связи нет, потому что это действие однократно.
Те примеры, которые вы привели, являются просто обычными парсерами, подключаемыми на этапе компиляции.
А именно, для того, чтобы программа могла исследовать и менять свою собственную структуру на этапе выполнения, приспосабливая её к своим входным данным.
Вы смешиваете в одну кучу и рефлексию, и динамическую кодонерацию, и горячую замену кода.
А "рефлексия на этапе компиляции" – это просто оксюморон.
И? С++никам теперь стоит собраться и написать пропозал в комитет для изъятия рефлексии из языка потому что г.@vadimr объявил её оксюмороном в комментариях на Хабре?
Для компилятора сам текст программы является входными данными.
Только вот речь идет не о компиляторе, а о программе, которая в момент компиляции может анализировать сама себя и менять себя в момент этой самой компиляции подстраиваясь под текущие условия.
Те примеры, которые вы привели, являются просто обычными парсерами, подключаемыми на этапе компиляции.
Во-первых, не всегда удобно подключать что-то внешнее на этапе компиляции. Именно поэтому мы и стали делать свой PEG-парсер.
Во-вторых, покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции".
Во-вторых, покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции".
Синтаксис свой определить, тоже мне фокус. Хотя сама цель сомнительная.
(define-syntax typedef
(syntax-rules ()
((_ tag val) `(,'tag ,val))))
И? С++никам теперь стоит собраться и написать пропозал в комитет для изъятия рефлексии из языка потому что г.@vadimr объявил её оксюмороном в комментариях на Хабре?
Ну неплохо было б. Но комитет C++ стандарты принимает же без учёта мнения специалистов, в отличие от некоторых других языков.
Синтаксис свой определить, тоже мне фокус.
Каким боком ваши упражнения в Лиспе относятся к C++?
Ну неплохо было б.
Годы идут, лисперы не меняются.
Каким боком ваши упражнения в Лиспе относятся к C++?
Никаким, к сожалению.
Они относятся к вашему вопросу “покажите как получить strong typedef посредством "парсера, подключаемого на этапе компиляции"”. И иллюстрируют тот факт, что никакой анализ текста программы, как бы его не называть, для этого не нужен.
Никаким, к сожалению.
Статья о том, что сделано в C++ для того, чтобы решать проблемы C++ников, но вы пришли сюда что бы порассуждать о том, как может быть в других языках? Конструктивно, что тут еще скажешь.
И иллюстрируют тот факт, что никакой анализ текста программы, как бы его не называть, для этого не нужен.
Тогда покажите как это сделать именно в C++. Ведь не нужен же это, как его, "никакой анализ текста программы". Вот и продемонстрируйте мастер-класс.
Это может быть конфиг. Активированные в билде фичи, версия, хеш коммита и т д. Они не должны и не могут меняться после сборки.
Раньше для такого приходилось писать кодогенератор, теперь можно использовать для конфига JSON без предобработки, который гораздо проще редактировать из других скриптов, чем исходник.
Или это могут быть статические ресурсы, которые не предполагаются к рантайм замене. Раньше их так же парсили из константы, но в рантайме, а теперь можно распарсить в compile time.
То что вы не можете придумать зачем фича, это ещё не значит, что она не нужна.
Или это могут быть статические ресурсы, которые не предполагаются к рантайм замене. Раньше их так же парсили из константы, но в рантайме, а теперь можно распарсить в compile time.
В чём практический смысл?
В том, что по ТЗ не нужна динамическая замена ресурсов.
Это может быть embedded, где файловой системы вообще нет и всё вкомпилировано в бинарник. Это может быть WASM, где такое тоже встречается. Это может быть портативный софт "всё в одном бинарнике". Динамическая погрузка ресурсов не бесплатна, не всегда возможна и не всегда нужна.
Просто нужно иметь возможность вкомпилировать в бинарник некие const данные, которые не будут меняться после сборки. Раньше надо было описывать такие данные в коде, тепеоь можно принять данные в любом удобном формате без дополнительных утилит, кроме компилятора.
Ну вот вы приводите пример с хешом коммита. И как его при помощи машинерии из статьи засунуть в бинарник? Откуда возьмётся файл с хешом? Генерировать внешним скриптом? Ну так тогда можно сразу c++-файл сгенерировать.
Я не случайно привёл в пример не просто один хеш коммита, а целый конфиг сборки.
Допустим, у нас есть какой-то редактор этого конфига и проще сохранять/загружать его в json, чем парсить какой-нибудь config.h. Особенно если конфиг сложный, содержит вложенные структуры и т д
Или мы качаем конфиг с внешнего сервиса, где его меняют через веб интерфейс люди вообще далёкие от программирования на плюсах, а CI/CD запускает сборку бинарника по нему.
Да, это всё уже сейчас решается кодогенерацией. Но это надо писать скрипты под конкретный проект, усложнять сборку и т д. А теперь можно просто положить JSON и компилятор сам всё сделает. Разумеется, constexpr парсер json должен быть из какой-то библиотеки, а не руками его писать.
Его может вообще не быть на этапе исполнения программы, он становится не нужен после сборки.
JSON может гененироваться внешней утилитой во время сборки, приходить из другой системы или писаться коллегой, который вообще не умеет программировать на C++.
Допустим, бекэндеры описывают в JSON какую-то схему данных, а в нашем приложении автоматически генерируется нужные DTOшки под неё во время сборки.
Раньше требовалось писать отдельный кодогенератор, а теперь можно это провернуть средствами компилятора.
Раньше требовалось писать отдельный кодогенератор, а теперь можно это провернуть средствами компилятора.
Раньше требовалось писать кодогенератор, а теперь придётся писать кодогенератор.
Ну в целом это он и есть, зато вот прямо рядом и в самом языке. Это вполне удобно потребность в этом была понятна ещё с момента изобретения moc в Qt. Через него, кстати, генерируется код для рантайм рефлексии как в Java.
Да, но теперь кодогенератор не требует установки отдельных скриптовых языков, усложнения пайплайна сборки и т д. Теперь он встроен в язык.
Даже без Java, делать подобные внешние определения - такое себе. Добавляет пространство для ошибок, уменьшает гибкость. Не видно во что компилируется макрос - надо лезть в внешний файл и убивает анализаторы кода.
Идея хорошая, поиграться / протестировать - за, в прод - боже упаси.
"менять свой собственный код в ходе выполнения программы" - всегда считал, что это "динамическое программирование".
Динамическое программирование - это вообще из другой оперы. Восходящее решение подзадач.
Ну кагбэ:
Рефлексия (отражение; холоним интроспекции, англ. reflection) — процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Это всё вокруг одного и того же.
если проект попроще чем очень большой или разбит на части, то еслиб репл ClangRepl.html(во точно репл для С++ с рефлексией и отключаемыми модулями или кодом прям на запущенном репле) работал почти как sly(или таже идея с clojure) то что-то около этого было бы удобно вроде, но только в случае удобной компиляции и прочее
запустил пачку json-ов и смотришь как она поочереди их запускает реплом посути, ну и там придётся всё равно под пачку json-ов что-то делать, но логично вроде
программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Т.е. когда вот такой код модифицирует структуру программы:
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 в коде не вызывается, и результат ее работы ни к чему не приводит. Вот прям "не верь глазам своим" (С) К.Прудков.
У вас тут две программы. Одна выполняется во время компиляции (макросы тоже например), а вторая программа - это то что получилось на выходе из компиляции. И вот первая вторую менять может. Но первая не может менять саму себя и вторая не может менять саму себя.
Одна выполняется во время компиляции (макросы тоже например)
Макросы не в тему от слова совсем.
Есть программа, она выполняется, в процессе выполнения меняет себя. Да, в compile-time. Поэтому и называется compile-time рефлексия.
Но выполнение есть. Что и требовалось в том определении, на которое вы сослались.
так компил базу данных можно а туже аст в json нельзя чтоли? тоесть она получая АСТ может еще как возможность код и в json по красоте кидать(наверно ну просто звучит как сериализация) что плохого-то
Хм... на основе .proto файлов нужно создавать классы
Вообще говоря выглядит как хороший полуфабрикат для генерации чего бы то ни было с использованием некоего PerlScript-а, который штампует заголовочники с #embed. Потенциально настроечно-конфигурационные макросы и CMAKE будут хорошенько пересмотрены. На очереди встроенный Bison-Flex и Regexp как вишенка.
Я так понимаю, у бустовиков вообще не принято смотреть по сторонам. Их либа ну самая топовая, ага. Сравнивать можно только с ней. Потом появляются такие вот открытия... Стандарт ещё не вышел, а они уже мячьтають)))
Будет ещё время помечтать, детишки. Стандарт в лучшем случае получится заюзать года через 4. Пока что можно не заниматься ерундой, а заюзать reflect-cpp, и поубивать наконец ваши бустовые лохмотья на 100к строк. А ещё лучше реализовать как минимум сетевую часть вашего приложения на более дружелюбном языке, а не на плюсах и не дай бог на этом вашем бусте беаст и асио)
Люди, жалующиеся на то, что рефлексия не рантайм. На статической рефлексии легко реализовать рантайм (собрать все поля и методы класса в служебные структуры, позволяющие косвенное обращение, а затем в рантайме их использовать). А вот наоборот не выйдет.
Так что статическая рефлексия является более общим и универсальным механизмом.
Это верно только для самого простого и бесполезного случая - перестановки полей.
В рантайме вы можете, например, средствами рефлексии обращаться к коду, которого на этапе компиляции еще не существовало, как драйверы jdbc в Джаве.
Конечно, в конечном итоге всё в большинстве случаев можно переписать на машинном коде без рефлексии и без самомодификации, но вопрос-то не в этом.
А вот наоборот не выйдет.
Любая операция времени компиляции может быть с тем же результатом выполнена в рантайме. В принципе, ничто не мешает реализовать целиком интерпретатор C++.
Ваще не проблема. Пишете свой класс Object (или используете какую-то библитеку), которые имеет виртуальные методы типа getMethods, getFields и от которого наследуются все классы, которые должны уметь в рефлексию. При этом реализация этих методов генерируется в момент компиляции соответствующего класса с помощью статической рефлексии в одну строчку (вызов какой-нибудь consteval функции или наследованием через особый хитрый шаблон).
Теперь вы можете обращаться через такую рефлексию к классам из других модулей, к которым у вас не было доступа во время сборки вашего собственного кода (ну, конечно, собрано оно должно быть компиляторами с одинаковым ABI, это уже ограничение самих плюсов). Главное, чтобы код Object и генератора метаданных был общий для всех (благодаря статической рефлексии он может быть в виде header-only библиотеки, которую ничего не стоит подрубить везде).
Что-то подобное уже реализовано в Qt (там можно вызывать методы и писать свойства объектов по их именам), но требует запуска специального препроцессора для генерации метаданных. Со статической рефлексией можно отказаться от отдельного компилятора, снизив порог входа и упростив использование.
Ну правильно, это и будет сделанная руками рефлексия. Только она не будет поддерживаться языком, и, как следствие, вам придётся везде наследоваться от этого вашего Object и руками писать косвенные вызовы. Также можно руками реализовать VMT и иметь ООП на голом Си и т.д.
Но при этом у вас возникнет две иерархии классов – родная плюсовая и ручная рефлективная, и расхождение между ними будет трудно обнаружимо.
Средства языка позволяют достаточно сахара для того, чтобы спрятать косвенные вызовы функции. Есть же шаблоны и перегрузка операторов. Можно написать MethodRef, который будет вызываться точь в точь как настоящий метод, только без проверки аргументов в compile time (обернуть все в std::any или аналог) перегружая operator() variadic template функцией.
На самом деле, видимым в хорошей реализации останется только наследование от Object. Но это одна строчка на класс, плюс философия плюсов не платить за то чем ты не пользуешься, так что даже на уровне языка если бы была динамическая рефлексия, было бы нужно её явно включать для каждого класса специальным ключевым словом. Потому что она не бесплатна в рантайме (по сути сделает обязательной vtable для класса).
Преобразование JSON в объекты C++ на этапе компиляции: демонстрация возможностей принятой в C++26 рефлексии