Pull to refresh
51
0.3
Send message

ИИ консультанты и правда задолбали.

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

В последний раз мне пришлось обращаться к другому ИИ (ChatGPT), чтобы составил идеальную реплику, вызывающую перевод на оператора. И то пришлось её отправлять 3 раза подряд. Своими силами не осилил выйти из вечного цикла.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Потому что SFINAE.

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

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

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

Здесь накладывается ещё возможность частичной специализации. В общем случае конструктора копирования может не быть (он компилируется в некорректный код), но есть частная специализация для int где конструктор реализован иначе и валиден.

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

Или собрать один раз руками, а потом использовать во всех проектах на этой машине.

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

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

Впрочем, даже так не обязательно. Это даст выигрыш лишь если преобразование исходника в AST занимает существенное время от общего времени компиляции. У вас есть информация о том, что в Rust именно парсинг узкое место? Потому что если нет, то никто не заставляет обращаться к AST нодам реализации, если мы компилируем не этот модуль. Можно пробежаться по верхам AST и, опять же, собрать одни декларации. Оверхед лишь на парсинг.

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

Сейчас это не актуально.

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

Например, в C и C++ выражение

A*B;

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

В Rust (и многих других языках) такой неоднозначности нет - декларации переменных всегда предварены ключевым словом let, имена типов всегда идут после двоеточия и т д. Можно распарсить исходник не имея представления о том, что есть тип, а что переменная.

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

если тип явно не указан - это намекает, что тут не всё так просто, как кажется, и следует изучить как туда что-то ещё попасть может

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

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

Чтобы не писать MyCoolObject obj = new MyCoolObject(). Или MyCoolObject obj = (MyCoolObject) anotherObject.

В общем, где тип и так указан в той же строке.

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

Но если подумать о примере автора.

var result = calculate();

System.out.println("Elapsed time: " + result.getElapsedTime());

System.out.println("Result: " + result.getValue());

А нужно ли мне здесь знать точный тип возврата calculate? Алгоритм то и так понятен (что-то посчитали, вывели инфу о прошедшем времени и значение результата).

В момент написания кода IDE подскажет какие у него методы. CI/CD уведомит, что код компилируется (если, например, кто-то изменит тип возврата calculate и там больше не будет нужных методов). А на ревью проверяется в первую очередь общая логика.

В ТЗ был вывод времени и значения. Код выводит время и значение. Готово.

Что значит неудачный тип переменной?

Если тип неудачный, то там и логика пострадает. Что придётся всё с переменной делать через одно место. Условно, искать значение в List перебором вместо выборки из Map.

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

А если по алгоритму непонятно, что с типами что-то не то, то скорее всего с ними всё то.

Когда это вносили в Java, ссылались на Kotlin/Scala/Go. Все эти языки имеют статическую типизацию с опциональным выводом типов. На Python или JavaScript никто не ссылался.

В Java нет алиасов типов.

Получается, либо велосипедить свой Optional или Map (если по факту нужно вернуть именно это), либо так.

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

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

В Java/Kotlin/Scala/Rust/C++ не обязательно (где-то с рождения языка, где-то, как в Java и C++ после обновления стандарта) указывать типы переменных, но переменная имеет тот самый выведенный тип и после того как он выведен, в неё нельзя класть ничего другого.

Касательно var в Java это вопрос кодстайла. Так же как и фабрика абстрактных фабрик фабрик тоже будет ужасна с точки зрения понимания человека, но Java никак не мешает этому с первой же версии. Всё хорошо в меру.

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

1
23 ...

Information

Rating
3,442-nd
Location
Франция
Registered
Activity