Pull to refresh

Comments 46

Все это хорошо, но...

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

Ну и как всегда, в С++ это выглядит как чрезмерно сложный код.

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

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

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

Динамическая рефлексия, это, например, когда из IR, можно скомпилировать код с инструкциями AVX-512, если они поддерживаются. Или без них, если не поддерживаются. То есть, в зависимости от хоста, на котором k8s поднял pod, получать разный код с разными инструкциями CPU. Пока что сам C++ это никак не поддерживает.

Звучит не как рефлексия. А что такое IR (infra red)?

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

Была бы обычная компиляция, если бы она не выполнялась динамически во время выполнения программы.

В данном случае, при использовании Roslyn или javac исходный код компилируется в IR (для CLR или JVM, соответственно). А уже во время выполнения рефлексия позволяет получать разный машинный код из одного и того же контейнера, в зависимости от CPU хоста, на котором k8s поднял pod.

Для C++ такая технология, в принципе, доступна через LLVM, но это выходит за пределы стандарта языка.

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

Ещё раз, это ни разу не рефлексия, а JIT - just in time компиляция. Это совершенно другая вещь, которая возможна только благодаря промежуточному платформонезависимому языку как результату первичной компиляции.

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

Я аппелирую к одному из первоисточников: Refection is the integral ability for a program to observe or change its own code as well as all aspects of its programming language (syntax, semantics, or implementation), even at runtime.

Как Вы собрались менять код программы без компиляции во время выполнения?

На C++ можно во время выполнения скомпилировать so и динамически её потом загрузить. Но это, как я уже указал выше, слишком тяжеловесное решение. На C# и Java это делается намного проще, как раз благодаря JIT.

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

Ну как хотите. Проповедуйте дальше свои догмы. Может и найдете паству, которая к ним прислушается )))

Это не догмы, а стандартные определения. И, кстати, надо разделять рефлексию и кодогенерацию.

Ключевой аспект рефлексии — возможность наблюдать (observe) свой собственный код, не более. И если мы говорим о рефлексии на C++, то мы должны уметь отображать код в конструкции языка C++ (упрощая, в этакое AST-представление, подобное кишкам компилятора), в компилтайме (или, вернее, в каком-либо constant evaluated-контексте) для статической рефлексии или в рантайме (или, вернее, без ограничения на constant evaluated, вероятно, скажем, по указателю-ссылке) для динамической.

Как Вы собрались менять код программы без компиляции во время выполнения?

Прямой записью байтиков с машинным кодом в память, например.

Для C++ такая технология, в принципе, доступна через LLVM, но это выходит за пределы стандарта языка.

И всегда будет выходить, потому что стандарт C++ написан достаточно абстрактным языком для того, чтобы понятие «генерация кода» не имело смысла.

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

Пример в чистом виде рефлексии - это компиляция из динамически формируемого исходного кода классов сериализатора/десериализатора во время выполнения программы при изменении схемы. Для эффективной обработки Protobuf на C# именно так и поступаем.

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

Прошу прощения, а каким определением рефлексии Вы пользуетесь?

Я пользуюсь классическим определением: "reflection allows a program to modify, even at run-time, its own code as well as the semantics and the implementation of its own programming language"

Или Вы считаете, что модификация машинного кода во время выполнения программы не соответствует этому определению? Тогда почему?

Ваше опреление рассказывает, что рефлексия позволяет днлать (и то весьма туманно), но не чем рефлексия является. А вот более прямое определение: "In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior".

И под modify тут понимается вовсе не генерация кода под CPU, а удаление/добавление членов классов или прямая замена имплементации методов, чтобы прежде всего изменить внешнее поведение. Я сам как-то заменял один метод во внешней библиотеке на C#, чтобы он вызывал нужную нам функцию.

Но изменение методов - это лишь 1% от общего числа использования рефлексии (можно даже сказать, что это в версии "Reflection Pro"), а 99% - это именно "examine, introspect".

modify its own structure and behavior

Ну так генерация машинного кода с использованием AVX или без него и есть модификация поведения программы. Что не так?

удаление/добавление членов классов или прямая замена имплементации методов

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

Ну так генерация машинного кода с использованием AVX или без него и есть модификация поведения программы.

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

Это будет верно, только если считать CLR, JVM или V8 компонентом ОС. Но даже в Android, где ART является действительно компонентом ОС, JIT оптимизация происходит по результатам профилирования уже выполняющегося скомпилированного при установке приложения кода. Так что тут опять видим пример самомодифицирующего кода.

Не самомодифицирующегося, а модифицируемого внешними средствами (которые как правило являются частью ОС или близкими к этому понятию системными программами - такими как CLR/JVM).

Из именно самомодифицирующегося (т.е. когда код модификации программы является частью программы и пишется разработчиком программы вместе с остальным кодом) приходит в голову разве что какие-нибудь полиморфные вирусы. Ах да, еще вроде бы в ATL/WTL для таблиц обработчиков событий применяется какой-то хитрый способ генерации кода под названием "thunks".

Наша дискуссия уходит в сторону субъективного понимания терминологии. Вы уже CLR, JVM и V8 отнесли к системным программам, что для мне вообще непонятно.

Я пользуюсь таким определением: "Самомодифицирующийся код — программный приём, при котором приложение создаёт или изменяет часть своего программного кода во время выполнения."

JVM, CLR, V8 и т.п. создают программный код во время выполнения и передают ему управление. Поэтому я и считаю их самомодифицирующими программами. По ссылке, кстати, в качестве примера самомодифицирующего кода как раз приведена и JIT-компиляция.

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

Спектр применения динамической рефлексии и, в частности, самомодифицирующего кода, на самом деле весьма широк: динамическое формирование шейдеров в 3D графике, динамическая компиляция DSP программ, динамическое создание классов сериализации/десериализации для новых или изменившихся схем, обучение математических моделей и т.п. Даже Linux Kernel самомодифицируется во время загрузки.

Вы уже CLR, JVM и V8 отнесли к системным программам

Почему бы и нет? Это в чистом виде расширения ОС. Как и драйверы. Только эти работают на другом уровне.

JVM, CLR, V8 и т.п. создают программный код во время выполнения и передают ему управление. Поэтому я и считаю их самомодифицирующими программами

Они модифицируют себя? Нет, другой код. Вот если бы они модифицировали свой собственный код, то да. А так - нет.

Динамическая генерация шейдеров пожалуй подпадает под это понятие.

Обучение моделей? Ну не знаю. Это все-же данные а не код. Так можно сказать что и конфиги с настройками это самомодифицирующийся код:)

Почему бы и нет? Это в чистом виде расширения ОС.

Назовите хотя бы одну ОС, в которой все эти компоненты будут установлены по умолчанию. В Windows будет CLR, но не будет ни node.js, ни JVM. В Android будет JVM (ART), но не будет ни CLR, ни node.js. При установке почти любого дистрибутива Linux ни JVM, ни node.js, ни, тем более, CLR тоже не обнаружите. Может не надо натягивать сову на глобус?

Как и драйверы.

По-вашему драйверы полностью в userspace? Или наоборот, JIT-компиляторы не в userspace?

Они модифицируют себя?

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

Обучение моделей? Ну не знаю. Это все-же данные а не код.

Вы точно занимались обучением моделей? Как Вы думаете, откуда берется машинный код модели, исполняемый на конкретном GPU или CPU?

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

Библиотека регулярок, которая по регулярке строит этакий байткод NFA и передаёт ему управление (и заодно содержит его интерпретатор) — это JIT, или рефлексия, или самомодификация?

Вы точно занимались обучением моделей? Как Вы думаете, откуда берется машинный код модели, исполняемый на конкретном GPU или CPU?

Из исходника на C++ (или вашем любимом языке), который вы скомпилировали вашим любимым компилятором.

Если я беру какую-нибудь нелинейную регрессию и «обучаю» её, находя коэффициенты Левенбергом-Марквардтом, то где здесь самомодификация?

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

Если я беру и обучаю random forest, то где там самомодификация?

Если хотя бы в одном из этих случаев есть самомодификация, то почему её нет в

int main(int argc, char**)
{
  std::vector<int> vec;
  if (argc > 2)
    vec.push_back(42);
}

?

чтобы это работало между библиотеками, должно быть единогласие, что надо генерировать

Вы удивитесь, но это называется "стандарт языка".

А какой смысл было вводить новый синтаксис типа баян с крышечкой [:^III:] ? Почему нельзя было использовать слова? (sizeof, elementsof, ...) Или слов не хватает для такого безобразия?
Более того что мешает добавить новые макросы типа #include но наоборот что бы генерировало файл с информацией о запрошенных элементах программы (типах шаблонах и т.п.) аля #export_details "info1.txt" selector(args,...) и уже на основе этой информации делать анализ или генерацию нового кода для программы. Нафига постоянно пытаются добавить побольше инопланетного синтаксиса. И вместо единообразия сделать всё максимально разношерстным? Вместо разделения разных задач на независимые более простые, смешивают всё в кучу, порождая ненужную сложность.

  1. Никто не заставляет пользоваться. В начале написано - если это есть, и ты этим не пользуешься, то ничего для тебя не изменится.

  2. Причешут. Лямбды, constexpr, списки инициализации причёсывали в три итерации после их релиза. Сейчас причёсывают модули, ranges, fmt уже вторую итерацию. Потом к ним добавится или нет статическая рефлексия. А сейчас там на этапе предложения вообще всё может поменяеться, не в первый раз.

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

конечно хорошо, что это появится хоть в каком то виде. но вот тоже непонятно это желание побольше всяких непонятных интуитивно обозначений использовать. почему не просто typeof(T)? также любопытно как это планируется в C++/CLI реализовать, где крышечка уже занята под reference.

Там T^ никак не конфликтует с ^T . Но всё идёт к тому, что будет ^^T. Мне уже пришлось адоптировать последний авторский код на Godbolt под ^^.

4.1.1 Syntax discussion

typeof() занят GCC, кажется

по хорошему, выпилить бы его оттуда в пользу decltype. а так вообще вопрос интересный: должен ли стандарт языка вообще оглядываться на расширения компиляторов и диалекты этого языка? вон выше автор поста ссылку привел на дискуссию по этой теме: https://habr.com/ru/articles/870750/comments/#comment_27735940

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

Я не знаю, как в C++20 пролезли char8_t, co_await, co_yield, co_return, concept, requires, consteval, constinit, import, & module. Это просто праздник какой-то. Не думаю, что в ближайшее время мы увидем новые ключевые слова.

Я уже понял почему. Они ввели переход между одним "миром" и другим. ^expr -> std::meta::info и обратно слайсер (боян) [: std::meta::info :] -> source. Аналог php "stream1 <​?php stream2 ?​> stream1" где правила разбора для stream1 и stream2 разные. Но ничто не мешало просто использовать строку "expr" и спец функцию std::meta::source( std::meta::info( "expr" ) ). Просто видимо показалось что это слишком просто. Крышечки и баяны сложнее в гугле искать, поэтому только так.
При этом с рефлекшеном по прежнему предлагается работать методами c++ и шаблонов. Что слегка удивляет. Логичнее было бы работать на основе запросов или селекторов типа sql или css. Более того было бы логично ввести внешнюю обработку типа плагинов на скиптовых языках. И добавить выгрузку std::meta::info во внешний файл для обработки или для генерации отчета или документации. Типа [: ^expr | filter("ext_script_file",args) :] и сделать псевдоним: script_alias(^expr,args).
Помимо обработки на этапе компиляции, есть еще обработка до этапа компиляции и после компиляции на стадии линковки. Например хочется иметь отчет о том что попало в исполняемый файл. Пока это возможно только с помощью парсинга map файла. Не говоря уже об управлении линковкой. Например на основе не определённых символов искать и подключать необходимые библиотеки для линковки. Или указать что всё что находится в namespace1 выделить в отдельную динамическую библиотеку или более сложные селекторы.

Более того было бы логично ввести внешнюю обработку типа плагинов на скиптовых языках.

Помимо обработки на этапе компиляции, есть еще обработка до этапа компиляции и после компиляции на стадии линковки.

Этого в стандарте не будет вообще никогда, боюсь.

Термин "Статическая рефлексия " дан максимально непонятно. Определение звучит как некая магия. В чем статичность? В чем отличие от RTTI Delphi ? или C#? Ну или раз автор не знает про рефлексию в этих языках, а упоминает Python и Java и говорит, что будет не так как там, то хотя бы уже хоть немного надо сказать в чем же разница. Информацию о типах будут генерировать не для всех доступных, а только для какой то части ? Вроде того что типов, для которых на этапе компиляции понятно, что будет использоваться информация о типах? Или для всех которые потенциально в generic могут быть использованы, тогда это тоже самое, что RTTI, только давайте назовем по-другому, чтобы по непонятнее было.

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

Хранит, потому что может быть не известно, что нужно будет, а что нет на момент сборки. Например, в программе может быть "точка доступа" к ядру программы через UI и что запросит пользователь и в каком виде не известно.

Термин "Статическая рефлексия " дан максимально непонятно. 

Как я понял, статическая значит работает на этапе компиляции, а точнее на этапах когда во время компиляции выполняется пользовательский код (а это и constexpr/consteval, и шаблонное метапрограммирование). Здесь и генератором, и потребителем метаданных в конечном итоге оказывается компилятор. А в рантайм эти метаданные могут попасть только если их туда специально затолкать, т.е. например использовать для инициализации рантайм-объектов.

RTTI и C# Reflection работают во время выполнения - там компилятор просто подготавливает константные метаданные и сохраняет их в exe-шнике, эти данные используются в рантайме для инициализации всяких reflection-объектов, обращение к которым происходит также в рантайме.

Что можно сделать макросами в С++? Таки всё:)

Сейчас это так, но в C++26 всё, вероятно, изменится. И то, что станет доступно в C++, будет сильно отличаться от того, что есть в таких языках, как Java или Python. Ключевое отличие - слово «статическая».

Не могу не отметить что это уже почти догоняет реализацию рефлексии в таких языках как D

Пример с копированием по именам полей...

Боже, чую в C++ появится свой automapper, и лет через 10 будут обсуждать является ли он плохой практикой.

Не пишу на C++, но в примере с копированием структур, копирование в цикле в один и тот же объект выглядит страшновато

Есть замечательная библиотека https://github.com/getml/reflect-cpp, которая реализует статическую рефлексию на C++20 через std::source_location
Без макросов, работает с enum'ами, классами.
Пример рабочего кода:

#include <rfl/json.hpp>
#include <rfl.hpp>
struct MyStruct {
  std::string field1;
  int someInteger;
  std::string field2;
};
int main()
{
  MyStruct data{"a", 123, "b"};
  std::cout << rfl::json::write(homer);
  return 0;
}

// output
{"field1":"a","someInteger":123,"field2":"b"}

Sign up to leave a comment.

Articles