Pull to refresh
22
0
Антон Жилин @Anton3

Пользователь

Send message
У непроверяемых исключений есть хороший юз кейс: нефатальные ошибки без возможности восстановления. То есть когда нужно а) откатить незавершенные операции и б) вывести «красивое» сообщение об ошибке. С RAII мы получаем код обработки ошибок почти бесплатно.

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

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


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

Разве это не аргумент в пользу сокращённых лямбд? Ведь они не копируют неявным образом.

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


template <typename R, std::invocable<element_t<R>> F>
auto filter(R&& r, F&& f);

Тогда если ваша лямбда не может "съесть" element_t<R>, то компилятор вам так и скажет.


Кстати, IDE стоит поддерживать концепт std::invocable и подписывать рядом с auto параметрами переданной лямбды то, чему эти параметры на самом деле будут равны. Примерно так:


filter(vector{1,2,3}, x int& : x > 0)


Жаль, сейчас ни одна IDE этого не делает. Надо закинуть идею разработчикам.

Идея "прозрачных лямбд" (transparent functions) предполагает, что выражение лямбды ведёт себя ровно так, как будто бы его вставили прямо на место вызова (за исключением того, что параметры вычисляются только один раз). Если вам нужна копия, приведите параметр к не-ссылочному типу. P0849 упрощает создание копии до auto(param).


Если вы передаёте лямбду в место, которое «еще всех нас переживет», то пропишите явный список захвата, сокращённые лямбды это тоже поддерживают.

Во-первых, сокращённые лямбды не предполагают явного указания типов параметров, возвращаемого типа и прочих атрибутов обычных лямбд — вместо этого лямбда автоматически ведёт себя ровно так, как её тело.
Например, &x, &&y: x += y ошибка — правильно x y: x += y.
Во-вторых, при таком синтаксисе, очевидно, нужно ограничение, что expression-statement не может начинаться с сокращённой лямбды, так как : используется в метках. Так что, как и сейчас:


  • :42; — это ошибка
  • x: x * 2; — это метка x: плюс x * 2;
  • x y: x + y; — это объявление переменной y типа x + далее ошибка

Плюс, вот ещё юзкейс — создание форматированной строки только в случае возникновения ошибки:


assert(a == b, :format("a != b, a={}, b={}", a, b));
assert(a == b, [&] { return format("a != b, a={}, b={}", a, b)); });

Представьте типичную задачу обработки набора данных на Ranges, с парой шагов map transform, filter и завершающим reduce accumulate. На обычных лямбдах:


auto result = iota(1, 1000)
    | transform([](int x) { return x * 2; })
    | filter([](int x) { return x % 3 == 0; })
    | accumulate(0, [](int x, int y) { return std::abs(x - y); });

На сокращённых лямбдах:


auto result = iota(1, 1000)
    | transform(x: x * 2)
    | filter(x: x % 3 == 0)
    | accumulate(0, x y: std::abs(x - y));

Во втором варианте в 2 раза меньше синтаксического мусора.


Ну или представьте некопирующий вариант заполнения вектора:


auto v = vector(10, [&] { return factory.make(); });
auto v = vector(10, :factory.make());

Разве не мило?


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

В первом примере, copy elision гарантируется для b, так как его potential scope (грубо говоря, время жизни переменной) включает statements на строках 4-5, среди которых все return возвращают только b. Гарантируется для a, так как в его potential scope (строки 8-9) все returns возвращают только a.


Во втором примере, аналогично, всё гарантируется. Когда мы делаем goto label, перепрыгивая до объявления переменной a, её объект уничтожается, так что на его место преспокойно может отправиться b.

В реализуемости уверен, но вот насколько сложно это будет реализовать в GCC или MSVC, понятия не имею. Control-flow анализ не потребуется: необходимо, чтобы на протяжении времени жизни переменной не возвращалось ничего иного. Такой анализ можно было бы практически через регулярку реализовать, если бы не условие, что "возврат иного" внутри нерабочих веток if constexpr — не в счёт.

Теоретически, можно при перемещении сделать relocation для всех членов и сделать какую-нибудь минимальную пометку о том, что объект moved-from, и его можно только удалить. Правда, нам для этого нужно выделить дополнительный bool, или знать tombstone bit pattern в одном из членов.

Внезапно понял, что auto&& сработает. Для возвращаемого типа функции это недопустимо, а для локальной переменной, где надо, сработает lifetime extension. Так что зря я нагородил :facepalm:


Хотя если мы потом вернём эту переменную с lifetime extension, то NRVO не сработает. Но тогда уж лучше прописать разрешение на это в NRVO.

Основная идея — сделать не-копирующие объявления короче копирующих. Второстепенная — корректность:


struct S { int x; }
S s{42};
decltype(auto) y = s.x;  // тип - просто int

Причина — неочевидное поведение decltype в целом и decltype(auto) в частности:
decltype(s.x) = int
decltype((s.x)) = int&


Убедитесь сами.

Основания. Хотя глупо это спрашивать — Trivially relocatable — это базовое свойство всего в си. Когда там это было никакого раста в принципе не существовало, но в любом случае я жду оснований.

В C, но не в C++. Когда при принятии C++11 думали, как уменьшить число копирований, почему-то результатом стала move-семантика. А сейчас обнаруживается, что в 99% случаев-то на самом деле нужен destructive move (или relocation, как его сейчас называют). Хотя в комитете сидят люди далеко не глупые. Значит, со времён C++11 тенденции в языках программирования поменялись. Насколько я знаю, именно Rust привнёс концепцию destructive move в массы.


Да и никаких понятий "relocatable" в расте в принципе нет

Relocatable — это понятие из C++. Аналог в Rust называется просто move и записывается как присваивание.


Зачем этот мусор в языке?

Эта конструкция отменила бы копирование как поведение "по умолчанию". Но я особенно над этим не думал. Возможно, да, фигню сморозил.

Всё довольно плохо. Из-за обратной совместимости, восходящей к Си, синтаксис местами уродлив, новые фичи прибиваются сбоку гвоздями. Зато куча библиотек и производительность. За бест практисами обращайтесь к CppCoreGuidelines.

Примерно. С decltype(auto) тоже есть проблема, об которую можно споткнуться: decltype(x.y) выдаст тип T, а не T&, если поле y типа T. Тут спасёт новая конструкция, которая корректно обработает такие случаи.

Полностью согласен. Trivially relocatable — калька с Rust, просто поняли, что там намного лучше. Abbreviated lambdas разрабатываются с учётом того, чтобы не копировать неявным образом параметры или результат; про обычные функции там тоже подумали. Было бы круто ещё иметь короткие не-копирующие объявления переменных:


x := f();               // станет?
decltype(f()) x = f();  // сейчас
Много идей лежат на поверхности. Можно стартовать голосование для каждого голосующего в рандомное время среди недели. После голосования можно будет проверить, что голос засчитан, исправить выбор, если нужно, и завершить голосование. А потом снова его начать, уже с начальником за спиной, но на этот раз голосование будет фейковым, хотя будет выглядеть правдоподобно. То есть проголосовать можно будет сколько угодно раз, но на самом деле будет засчитан только первый голос.

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

"Выиграть" в данном контексте означает проанализировать экспоненциальное число партий и найти выигрышную стратегию при условии идеальной игры противоположного игрока.

Обработка ошибок станет если не явной, то по крайней мере, предсказуемой. В статических исключениях будет аннотация throws для функций, которая будет означать "да, я кину исключение, обработай это, пожалуйста". Далее IDE сможет подсвечивать вызовы таких функций, см. пример выше. Сейчас так делать невозможно, потому что расставлять noexcept разработчики "ленятся". Кто-то считает, что нельзя отдавать такие вещи на откуп IDE, и нужно ещё и ключевое слово try при всех вызовах бросающих функций. Мне кажется, что это уже лишнее.

char = текст ASCII
chat8_t = code unit из UTF-8
int8_t, uint8_t = целые числа
std::byte = неизвестные данные (не текст и не числа)
wchar_t = системный тип символа (привет, ужасы кодировок Windows)
signed char, unsigned char = что угодно в устаревшем коде; стоило бы выпилить
1

Information

Rating
Does not participate
Location
Люберцы, Москва и Московская обл., Россия
Date of birth
Registered
Activity