Обновить

Комментарии 103

А boost еще используют с новыми стандартами?

Посмотрел на boost libraries. По частоте использования стандартов - на первом месте С++03, на втором месте С++11, на третьем месте С++17, на четвёртом месте С++98 (здесь надо вставить эмодзи с выпученными глазами). Возможно что-то упустил, но это по мелочи.

ага при этом буст в псоледних версиях прекратил поддержку С++03

Ну, у буста вроде есть ли в и для C++20.

Я использовал как минимум boost.asio в проекте на С++23

Хорошо, допустим я вкачусь в C++23, буду самым модным в индустрии

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

Мне расскажут как в компании используют новые стандарты, о новых фишках в их проекте

Таким образом выйду на свой первый рабочий день и увижу C++03, C++Builder. Увижу что auto вместо дедукции типа делает подстановку int. Увижу что все пользуются своей имплементацией std::string, std::unique_ptr

И что мне делать? Увольняться и искать самую модную компанию? А что если мне на кодревью скажут провести бенчмарк между подходом с ranged/views и самым обычным проходом? Может компилятор сделает хорошую векторизацию, так как у него было 20 лет для выявления оптимизаций на простейшем цикле

Может мне стоит прочитать книги дедов и разобраться хотя бы что такое bss? Почитать про паттерны?

А может ли быть такое что std::unordered_map сделана через одно место и будет куча коллизий с пустого места? А какие контейнеры сделаны дедами в pbds?

Посыл правильный, что нужно пользоваться инструментами языка, а не писать на языке "С+"

Но при чем тут изучение с нуля? В чем будет разница между профессионалом, который не знает ничего что было до нового стандарта и claude code?

Особенно про auto хорошо.

auto пишут не для дедукции типа, а потому что 4 буквы. Ленивые люди не пишут тип и потом танцуют на граблях. Ещё с auto плохо работают дополнения.

Что насчет auto в параметрах лямбда-функций?

Пишу на c++ 25 лет. Считаю, что auto - очень правильно. Зачем повторять тип всегда (иногда стоит), если, конечно, платят не за количество знаков?

Соглашусь. Но тут надо помнить о ловушке. Например Foo& getFoo() записать как auto foo = getFoo() можно подорваться на мине и получить копию вместо ссылки.

И вот кстати у меня старший коллега прямо бесился с auto и требовал от меня писать тип полностью мол так когнитивно проще код понимать. А у меня наоборот глаза вытекают от количества букв которые запрещали спрятать под auto. Благо я с этим человеком уже не работаю.

Хорошо, что Visual Studio об этой ловушке предупреждает.

Если правильно помню, можно написать decltype auto, тогда ссылка никуда не денется.

Пока не пишешь итераторы или типа того – можно себе позволить писать типы. Но так-то использование std без auto – не то, чем стоит заниматься.

Таким образом выйду на свой первый рабочий день и увижу C++03, C++Builder

Это довольно сказочный кейс - устроиться на работу так и не узнав про рабочий стек. Не представляю как так можно устроиться на работу и узнать о том что у них C++ Builder в первый рабочий день, а не на техническом собеседовании. Особенно после рассказа о том как используют новые стандарты. Я бы вообще советовал бежать от такой кампании, если такое действительно случилось.

Увижу что все пользуются своей имплементацией std::string, std::unique_ptr

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

бенчмарк между подходом с ranged/views и самым обычным проходом

ну если вы джуном пойдёте, то наверное за бенчмарки вам расскажут. Если вы побородатее, то кажется написать простейший бенчмарк через дельту std::chrono::now() не сложно. Ну или хотя бы на том же godbolt подсмотреть, как они их гоняют, а то и прямо с него примеров показать. Не очень понятно что вы хотели этим сказать.

разобраться хотя бы что такое bss

а как информация о бинарных секциях поможет человеку с разработкой? туда же вопрос про переизобертение какого-нибудь visitor / flyweight / fabric / etc из книжки про паттерны без использования концептов/шаблонов.

А может ли быть такое что std::unordered_map сделана через одно место и будет куча коллизий с пустого места? А какие контейнеры сделаны дедами в pbds?

это вообще странный йвопрос - как можно изучать язык и не изучать стандартную библиотеку? понятно, что нюансов там найдётся немало, но тут сильно зависит от уровня программиста. Странно было бы выкатывать зелёному разрабу претензию про то что он чего-то не знает/не учёл. Ну и в условный HFT с таким уровнем знаний нюансов всё равно не взяли бы, так что о чём бугурт?

Навскидку, если в геймдев на UE5, то это cpp 17 и выше. И современная ide.

А можно не хрюкать, а под клод код на новом стандарте рубить $. Ну а можно как ты - читать пыльные, пахнущие пуками дедов книжки. Ммм..smells like spring

Неожиданно прибежали янычары понадобилось взаимодействие с ОС на уровне API...

Вот тут с современными стандартами стало как раз только лучше. Особенно из-за компил-тайм вычислений. Пишешь constexpr прокладку и всё работает в уровне абстракций c++20 с zero-cost HAL

Моя цель - научиться писать структурированные, понятные статьи без использования LLM за полгода-год

Для этого лучше сразу писать статьи без LLM.

По этой статье: я понял, что вы хотели сказать, потому что я понимаю C++, но для того, кто не понимает, структура статьи, как мне кажется, выглядит так: C++ раньше был страшным, потом стал не страшным, смотрите, вот один непонятный пример кода, теперь другой непонятный пример кода, и на будущее запомните: если вам рассказывают про непонятное слово new, то не слушайте, а непонятные слова std::unuque_ptr и std::shared_ptr - это хорошие слова, про них слушайте. Человек, наверное, поймёт, что в C++ есть "старое плохое" и "новое хорошее", не больше. Я не говорю, что я мог бы сделать лучше, я думаю, что про C++ кратко рассказать невозможно, и сам не берусь.

Вы абсолютно правы. У меня, как у человека заинтересованного в плюсах на уровне хобби, такое впечатление и осталось: есть вот плохой С++, а есть хороший. То не учите, а это учите. Хз, на сколько информация правильная в статье. Можно ли сравнить разницу между разными версиями С++ и, например, питон 2 и 3?

Спасибо большое за фидбэк! Да, получилось сумбурно, понимаю. Моя цель улучшить навык составления текстов, это первый опыт)

Забыли добавить ссылку

https://google.github.io/styleguide/cppguide.html

Как old school C++98 programmer с нетерпением жду выхода C++26, где нас ждут грандиозные нововведения.

Поэтому, забудьте С++98, С++11, С++17, С++20 - начинайте сразу с С++26! 😁

Чё ждать то, текст стандарта?). В gcc trunk рефлексия есть, можно пробовать.

Уже в gcc в релизе 16.1 есть рефлексия

А, и у тебя может выстрелить std::bad_alloc() во время вызова new, но об этом добрый дедушка вряд ли вспомнит.

Отлично, вот вы вспомнили и что? Чем C++20 здесь лучше старого доброго “Си с классами” (aka C++98)?

RAII. Приведу пример кода.

type* ptr = new type;
type* ptr2 = new type;

delete ptr2;
delete ptr;

Вполне может вылететь исключение из второго вызова new. Тогда получится так, что delete для ptr никогда не вызовется. Вот и утечка нарисовалась. Разумеется, так вылететь может не только внутри new, но и много где ещё (хотя оно и не вылетит при использование "Си с классами"). Хотя если рассматривать сценарий, где нехватка памяти это сразу полное завершение программы и там и там, то разницы особой не будет.

RAII

Вы думаете, что RAII появилось только в “modern C++”?

Вполне может вылететь исключение из второго вызова new.

Вы думаете, что до “modern C++” программисты про exception safety не задумывались?

Вы думаете, что для иллюстрации проблемы вам нужен первый new? Вместо первого new может быть что угодно – открытие файла, захват mutex-а, создание сокета и т.д., и т.п.

Когда программист озадачивается обеспечением exception safety, ему не важно, из new летит исключение или еще откуда-то. А если не озадачивается, то тут же без разницы, C++98 у него в руках или C++26.

хотя оно и не вылетит при использование “Си с классами”

Тут бы определиться с тем, что вы называете “Си с классами”. Есть несколько вариантов:

  • это тот прототип языка, который был до 1983-го года, когда название C++ вообще появилось;

  • это тот вариант нестандартизированного C++, который был до C++98;

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

И с какой стороны это RAII ?

При RAII не используются ни сырые поинтеры, ни явное delete.

RAII - это идея инициализации переменных во время из объявления, то есть это RAII.

RAII - это идея инициализации переменных во время из объявления, то есть это RAII.

Интересная интерпретация идеи RAII. Рискну предположить, что еще и полностью бесполезная, а даже и в чем-то вредная.

RAII - это идея инициализации переменных во время из объявления, то есть это RAII.

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

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

Надеюсь более менее попал в цель.

Не увидел здесь применение RAII.

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

А этот пример, легко переписать через какой-то стандартный тип указателя из std:: чтобы гарантировать exception safety

в C98/03 можно даже туже семантику перемещения сделать это не так сложно как кажется на первый взгляд и даже туже decltype и свой аналог auto что вроде anytype

Говорят, что и брюки через голову можно одевать.

Может, все-таки надевать? А то непонятно, где у брюк голова ;-)

А что, компиляторы уже поддерживают все фичи c++20, в т.ч. модули?

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

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

Ну пока самая идеальная имелементация модулей в MSVC, затем идёт clang.

По новому вы 2 раза цикл обходите. Получается, что для чего то серьезного надо по старому писать

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

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

Вот не надо таким "разработчикам" на C++ программировать.

А почему, если не секрет? Ну кроме того, что для них есть питон...

потомучто есть zig/Rust по моим тестам по кешу они почти 1 в 1, можно конечно сначала либу написать на С/С++ и потом продолжить на Раст, или на Раст написать либу и пользоваться ею на С++/python

Скрытый текст
const std = @import("std");
const mxg11l = @import("project");//обёртка над X11+GL 

const Quad = struct {
    vao: u32,
    vbo: u32,
    ebo: u32,
    pub fn init() !Quad {
        const vertices = [_]f32{
            0.5, 0.5, 0.0, // Право верх
            0.5, -0.5, 0.0, // Право низ
            -0.5, -0.5, 0.0, // Лево низ
            -0.5, 0.5, 0.0, // Лево верх
        };
        const indices = [_]u32{
            0, 1, 3, // Первый треугольник
            1, 2, 3, // Второй треугольник
        };

        var vao: u32 = undefined;
        var vbo: u32 = undefined;
        var ebo: u32 = undefined;
        mxg11l.glGenVertexArrays(1, &vao);
        mxg11l.glGenBuffers(1, &vbo);
        mxg11l.glGenBuffers(1, &ebo);

        mxg11l.glBindVertexArray(vao);

        mxg11l.glBindBuffer(mxg11l.x11.GL_ARRAY_BUFFER, vbo);
        mxg11l.glBufferData(mxg11l.x11.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, mxg11l.x11.GL_STATIC_DRAW);

        mxg11l.glBindBuffer(mxg11l.x11.GL_ELEMENT_ARRAY_BUFFER, ebo);
        mxg11l.glBufferData(mxg11l.x11.GL_ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, mxg11l.x11.GL_STATIC_DRAW);

        mxg11l.glVertexAttribPointer(0, 3, mxg11l.x11.GL_FLOAT, mxg11l.x11.GL_FALSE, 3 * @sizeOf(f32), null);
        mxg11l.glEnableVertexAttribArray(0);
        return Quad{
            .vao = vao,
            .vbo = vbo,
            .ebo = ebo,
        };
    }
    pub fn draw(self: *Quad) void {
        mxg11l.glBindVertexArray(self.vao);
        mxg11l.x11.glDrawElements(mxg11l.x11.GL_TRIANGLES, 6, mxg11l.x11.GL_UNSIGNED_INT, null);
    }
    pub fn deinit(self: *Quad) void {
        defer mxg11l.glDeleteVertexArrays(1, &self.vao);
        defer mxg11l.glDeleteBuffers(1, &self.vbo);
        defer mxg11l.glDeleteBuffers(1, &self.ebo);
    }
};

pub fn processEvent(ev: mxg11l.Event) void {
    switch (ev) {
        .Key_Press => |data| {
            std.debug.print("Нажата клавиша: code={d}\n", .{data.keycode});
        },
        .Mouse_Move => |pos| {
            std.debug.print("Мышь на: {d}, {d}\n", .{ pos.x, pos.y });
        },
        .Window_Quit => |qev| {
            mxg11l.set_running_false();
            std.debug.print("Пока-пока! Статус: {any}\n", .{qev.quit});
        },

        else => {}, // Для остальных событий, которые пока не важны
    }
}

pub fn main() !void {
    var win = try mxg11l.BaseWindow.init();
    defer win.deinit(); // Закроется только при выходе из main!
    var event: mxg11l.Event = undefined;

    const vertex_shader =
        \\#version 330 core
        \\layout (location = 0) in vec3 aPos;
        \\void main() {
        \\  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
        \\}
    ;
    const fragment_shader =
        \\#version 330 core
        \\out vec4 FragColor;
        \\void main() {
        \\  FragColor = vec4(1.0, 0.5, 0.2, 1.0); // Оранжевый Zig
        \\}
    ;

    var quad = try Quad.init();
    defer quad.deinit();

    const shaderProgram = try mxg11l.createShaderProgram(vertex_shader, fragment_shader);
    defer mxg11l.glDeleteShader(shaderProgram);

    while (mxg11l.is_running()) {
        try mxg11l.poll_events(&win, &event, processEvent);

        const km = win.getKeymap();
        if (mxg11l.isKeyPressed(km, .esc)) { //
            //std.debug.print("ESC зажат! (обнаружено через опрос)\n", .{});
            mxg11l.set_running_false();
        }
        // --- Рендеринг ---
        mxg11l.x11.glClearColor(0.22, 0.44, 0.66, 1.0); // Красивый темно-бирюзовый фон
        mxg11l.x11.glClear(mxg11l.x11.GL_COLOR_BUFFER_BIT);
        // 3. Рисуем квадрат
        mxg11l.glUseProgram(shaderProgram); // Нужно скомпилировать заранее
        quad.draw();
        // --- Ожидание VSync ---

        mxg11l.x11.glXSwapBuffers(win.display, win.window); //_ = try std.posix.poll(&.{}, 10);

    }
}

они с растом рука об руку, я советую сначала Раст сразу изучать, далее чтобы понять новые концепции можно зиг смотреть )

попутно изучая это всё, настоятельно рекомендую смотреть кешгрнид на линуксе например, через валгринд командой

valgrind --tool=cachegrind --cache-sim=yes ./name

в целом для С/С++ зиг прекрасное дополнение, потомучто уже есть инфраструктура - тулкитов вокруг хаоса компиляторов типо

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

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

omfg. Мне одной только вертикальной палки в объявлении even_numbers хватает, чтобы закатить глаза и сказать: "что тут, вашу мать, происходит?"

Это синтаксический сахар для композиции функций.

Object | F1 | F2 эквивалентно

F2(F1(Object)).

@antoshkkaа не подкините годной литературы людям, которые хотят писать нормальный современный код?

Книги Rainer Grimm неплохи.

C++20 для программистов

Поддерживаю. Хорошо все новые темы объяснены. По этой книге разобраться, что такое ranges и зачем они нужны - часа 2...4. По cppreferences - дни, если повезет.

Beginning C++23 (7ed) by Ivor Horton, Peter Van Weert

23 года стандарт, а уже 7е издание!

Современные плюсы действительно достаточно приятны. Вот те же концепты, рейнджи, constexpr, модули, deducting this, корутины (которые, впрочем, не самые удобные), фолды, #embed и прочее.

Увы мир C++ настолько разнообразен , что в 2026 лучше с нуля учить Rust или что угодно не C++.

У нас есть проекты где до сих пор C++17, есть где запрет исключений, есть где запрет RTTI, есть где запрет ranges, лямбда и т.д.

Вопрос в том стоит ли тратить своё время на это сегодня когда модельки худо бедно сделают работу лучше чем кто-то с небольшим опытом плюсов.

У нас есть проекты где до сих пор C++17, есть где запрет исключений, есть где запрет RTTI, есть где запрет ranges, лямбда и т.д.

И есть проекты, где запрет всего (по MISRA C++).

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

Это смотря какую работу.

Например простую работу как покрыть код тестами моделька делает на ура.

Берём старый код за пару минут получаем тесты. Гарантировано находятся баги.

На и дальше можно развивать код гораздо спокойнее.

как моделька напишет правильный тест если нет хорошей документации на старый код? что чаще всего и происходит. В таких условиях, думаю, она просто прочтет импоементацию и подстроит все тесты под нее. Хорошо конечно - если в будущем что-то сломаете, то может найдет. Но баги, которые уже есть, так не найти.

Что такое правильный ?

И цель то не создать новое, а тупо зафиксировать текущее положение вещей, чтобы любое изменение было зафиксировано.

Положим у нас есть

def sum(a,b): return a-b

Пишем тест, который проверяет там sum(1,1)==0

Зафиксировали текущее положение.

Далее, скажем, поняли мы, что надо исправить. Изменяем “-“ на “+” и получаем падающие тесты.

Без тестов мы бы не знали вообще как изменение влияет !

Ну и в конце обновляем тесты: sum(1,1)==2.

Я не утверждаю, что модельки сделают всё идеально за нас

Зато помогают здорово экономить время на некоторых вещах.

Что такое правильный ?

очевидно это поведение которые вы закладываете в функцию, когда пишете ее.

И цель то не создать новое, а тупо зафиксировать текущее положение вещей, чтобы любое изменение было зафиксировано.

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

Зато помогают здорово экономить время на некоторых вещах.

Экономить то экономит, но есть сомнения в качестве таких тестов. По опыту, тесты написанные людьми итак не особо любят ревьюить - есть и есть. В итоге их качество оставляет желать лучшего. А тесты написанные LLM и сам разработчик думаю часто не ревьюит. Печально, но это типичная ситуация при разработке современных продуктов - важнее скорость, чем корректность / не качество, а количество.

для нового кода как раз подходит ненавистный TDD.

https://www.aihero.dev/skill-test-driven-development-claude-code

И получаем как раз то, что и хотели изначально.

Тесты не знают как устроено, код пишется пол тесты. Тесты создаются в соответствии со спецификацией.

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

Кстати, модельки и C++, а если к нас шаблоны и макросы и прочее, это ещё та затея :)

Надо несколько итераций делать, тогда будет лучше

За что лямбды запрещены? Это же вообще C++11, даже не 17.

Чото не современные примеры, где println? А, не забудь потом посмотреть сколько строк на ассемблере добавит эта фигня и как быстро будет билдится код на модулях, если будет вообще. Ну да, люди начавшие с цпп20 не смогут заметить косяков и не поймут в чём разница

Я начинал с C++11 и C++20, C++23 и C++26 считаю одними из лучших вещей, что происходили с языком. Модули вообще очень удобная и понятная штука.

А потом приходишь на РАБоту, а там 11, ну максимум 17 стандарт, гык, гык

Придётся спускаться с гор...

И это JS-но😁 я про мутации синтаксиса :)

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

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

Давайте сравним:

Как мы напишем это сейчас...

Как-то так:

fn main() {
    let mut numbers = vec![5, 2, 8, 1, 9, 4];
    numbers.sort();
    numbers.iter().filter(|n| *n % 2 == 0).for_each(|n| print!("{n} "));
}

Я тоже из числа тех, кому плюсплюсы зашли тяжело. Я учил Си по K&R в середине девяностых после Фортрана и Паскаля, но потом чёрт меня дёрнул взять в университетской библиотеке C++ Страуструпа, и знаете, вот насколько книга Кернигана и Ритчи была хороша, настолько книга Бьёрна была для меня ужасна, он, пожалуй не очень умеет хорошо объяснить... Так что по каким материалам учить и как - это тоже важно. Сейчас изучаю Раст и наслаждаюсь, реально.

Полностью согласен! C++ Страуструпа - это худшее начало для юного С++сника, которое только можно придумать.

Разработчику больше не нужно ... бояться обратиться за пределы массива

Выход за пределы массива всегда является ошибкой, на каком бы вы языке ни писали. Единственное облегчение - сейчас её легче найти.

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

Вопрос в том, когда она определяется:

  1. Во время компиляции всегда.

  2. Во время компиляции, если сойдутся звёзды у статического анализатора компилятора.

  3. Во время выполнения в момент выхода за границы.

  4. Во время выполнения где-то в другом месте через 100500 тактов, потому что проезд по памяти прошёл незамеченным.

Плюсы успешно балансируют между (3) и (4).

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

А во-вторых - я не понял, ты хочешь сказать по поводу п. 3 и 4. Я всего лишь написал, что выход за пределы массива является ошибкой ВСЕГДА. И поэтому его всегда надо бояться и исправлять. Ты с этим не согласен? Или что ты хотел сказать своим комментарием?

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

Что делается намеренно? Что потребовалось? Выход за пределы массива?!

Да, и возможно что это валидно. Например массив может автоматически аллоцировать бо́льший размер в этом случае. Мап так работает, хоть и не массив. Ещё индекс бывает отрицательным, и это не будет выходом за пределы, опять же

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

Требуется, конечно, потому что это зависит от системы типов языка.

современные компиляторы находят эти ошибки и при компиляции чистого C.

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

Или что ты хотел сказать своим комментарием?

Я хотел сказать, что, ну, вопрос в том, когда эта ошибка определяется (первая фраза-не-цитата в моём комментарии). Иными словами, как рано вы о ней узнаёте. И узнать во время написания кода или при компиляции сильно лучше, чем узнать во время выполнения через экзепшон из std::vector::at и тому подобные, что, в свою очередь, ещё лучше, чем узнать (если повезёт) из кордампа в совсем другом месте, что [] обратилось куда-то не туда.

Требуется, конечно, потому что это зависит от системы типов языка.

А какая система типов позволяет контролировать валидность индексов когда нужно бежать по главной диагонали матрицы размерности M и обрабатывать элементы в радиусе N от элементов (i, i), но при этом M и N определяются в ран-тайме?

Зависимые типы, например.

Можно пример? Как на зависимых типах вы обеспечите валидность индекса (i-N/2) когда значения i и N известны только в ран-тайме?

В данном случае — прямой проверкой. У меня есть функция, упрощая, возвращающая либо результат i - N / 2, либо доказательство, что i < N / 2.

Но если я где-то раньше проверил, скажем, что 2i ≥ N, то я могу воспользоваться этим фактом, чтобы ничего не проверять здесь, экономя время в рантайме.

Очень интересно, но ничего не понятно. У вас i и N определяются в рантайме. Что за функция, которая магическим образом возвращает нечто корректное – хз.

Не магическим.

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

isLT : (m, n : Nat) -> Dec (m `LT` n)
isLT _ 0 = No absurd
isLT 0 (S _) = Yes (LTESucc LTEZero)
isLT (S m) (S n) = case isLT m n of
                        Yes prf => Yes (LTESucc prf)
                        No contra => No (\(LTESucc x) => contra x)

Дальше с этим доказательством можно безопасно индексировать массивы. Можно статически (без рантайм-проверок) из доказательства m < n получить доказательство m < n + 1. Или, если вы знаете, что m > 0, что m - 1 < n.

Или, если вдруг m строится пусть даже в рантайме, но согласно допустимому диапазону индексов (ну как iota, например, от 0 до размера вектора), то рантайм-проверки тоже не нужны.

Речь шла про валидность (i - N/2), а не про отношения между m и n.

Мне кажется, что вы не може сказать нормальным языком простую штуку: типа у нас есть функция, которая берет i и N, и возвращает либо валидный индекс, либо признак, что индекс невалиден. А далее валидный индекс можно отдавать в оператор доступа к содержимому массива и при этом уже никаких новых проверок не требуется. Грубо говоря, на C++ это выражается чем-то вроде:

struct valid_index { std::size_t _index; };
struct invalid_index {};

using index_validity = std::variant<valid_index, invalid_index>;

[[nodiscard]]
index_validity element_at(std::size_t i, std::size_t N) {
  ... // Здесь разбираемся с (i - N/2) и возвращаем либо valid_index,
     // либо invalid_index.
}
...
// Где-то в коде, где нам нужно обращаться к (i-N/2):
std::visit(
  // За неимением паттерн-матчинга приходится применять костыли.
  overloaded{
    [](valid_index v) {
      // Уверены, что на руках у нас валидный индекс.
      ... // обращение по v._index.
    },
    [](invalid_index) {...}
    },
  element_at(i, N));

Т.е., наверное, для знатоков Haskell-я и Agda c Idris-ом, наверное, ваш язык понятен. Но вот людям от сохи, вроде меня, совсем нет.

Речь шла про валидность (i - N/2), а не про отношения между m и n.

Я надеялся, что если у вас есть инструмент для сравнения произвольных m и n, то вы сможете им воспользоваться для сравнения (i - N/2) с нулём (или, вернее, i с N/2, но неважно).

Мне кажется, что вы не може сказать нормальным языком простую штуку: типа у нас есть функция, которая берет i и N, и возвращает либо валидный индекс, либо признак, что индекс невалиден.

Потому что здесь ключевой вопрос — что такое «валидный индекс» (или «признак невалидности»). В частности…

Грубо говоря, на C++ это выражается чем-то вроде

…на C++ выражается не это. На C++ выражается что-то ближе к «всё честно, мамой клянус!»: компилятор не проверяет валидность индексов, корректность element_at, и так далее.

valid_index { v.size() + 1 } — существующий терм, компилятор не даст вам за такое по рукам. Функция

index_validity element_at(std::size_t i, std::size_t N) {
  // нету проверки
  return valid_index { i - N / 2 };
}

принимается компилятором, он от неё не расстроится и даже ворнинга не покажет. Функция

index_validity element_at(std::size_t i, std::size_t N) {
  // нету проверки, но в другую сторону
  return invalid_index {};
}

тоже принимается.

Для того, чтобы читающему код убедиться в корректности, ему нужно посмотреть на реализацию element_at (и всех других функций, принимающих, возвращающих и преобразующих valid_index / index_validity). Мне на реализацию isLT смотреть не нужно, мне достаточно её типа (и проверяемой тайпчекером тотальности), чтобы убедиться, что она делает что обещает.

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

Но вот людям от сохи, вроде меня, совсем нет.

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

Я надеялся, что если у вас есть инструмент для сравнения произвольных m и n, то вы сможете им воспользоваться для сравнения (i - N/2) с нулём (или, вернее, i с N/2, но неважно).

С чего бы на это надеятся если я совершенно не понял вашего объяснения.

Потому что здесь ключевой вопрос — что такое «валидный индекс»

Да уж вопрос вопросов.

С чего бы на это надеятся если я совершенно не понял вашего объяснения.

Потому что объяснение было механизма, а не самой концепции «можно доказуемо сравнить два числа».

Да уж вопрос вопросов.

Ну вообще да: в вашем коде это просто вопрос соглашения между программистами: мы договорились, что valid_index точно валидный, честное слово!

Потому что объяснение было механизма

Объяснение было? Простите, если оно было, то оно было расчитано на людей, гораздо более умных и эрудированных, чем я, поэтому оно прошло совершенно мимо меня.

Ну вообще да

Ну вообще нет.

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

Мой код – это всего лишь попытка перевести то, что я (не)понял из ваших объяснений и примера кода на неизвестном мне языке программирования на псевдокод на C++ (который, полагаю, гораздо лучше понятен читателям данной статьи). А именно – вводится тип, который означает “валидный индекс” и работа далее ведется именно через экземпляры этого типа, посему дополнительные проверки при обращении к вектору посредством таких экземпляров уже не нужны.

Объяснение было? Простите, если оно было, то оно было расчитано на людей, гораздо более умных и эрудированных, чем я, поэтому оно прошло совершенно мимо меня.

Желание не понимать начинает становиться воинственным, ну да ладно.

А именно – вводится тип, который означает “валидный индекс” и работа далее ведется именно через экземпляры этого типа, посему дополнительные проверки при обращении к вектору посредством таких экземпляров уже не нужны.

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

То, что вы предлагаете и называете переводом — это как если бы вы объясняли джаваскриптеру концепцию проверки типов C++, говоря, что предлагается просто писать

function myFun3(iAge, sName, dHeight)

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

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

Желание не понимать начинает становиться воинственным, ну да ладно.

Когда-то вам показалось, что потратить молодость на изучение С++ – это хорошая идея. Потом вам показалось, что приходить в комментарии к статьям про С++ и рассказывать какое C++ говно – это хорошая идея. Теперь вам кажется, что в моих словах есть что-то воинственное.

Попробую еще раз донести до вас простую мысль: если вы хотите объяснить что-то собеседнику, то хорошо бы это делать на том уровне и на том языке, который понятен вашему собеседнику (а не вам лично). Я вот даже не понял что за язык использован в вашем примере кода – Haskell, Agda или Idris. Соответственно, для меня нет ни вашего примера, ни вашего пояснения.

Я уже подробно написал выше, почему этот перевод так себе.

То, что вы написали, есть не более чем “доколупаться до столба”. Мой пример призван был всего лишь показать, что индекс, который программа может считать безопасным для использования, выражается отдельным типом (а не простым size_t или int-ом, как это сейчас массово встречается в реальном C++ном коде). А производят экземпляры этого типа специальные функции, внутри которых делаются необходимые проверки.

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

Жаль, что понимания таких простых вещей у вас нет, но т.к. общаюсь с вами не в первый раз, то ничуть не удивлен.

в этот раз попробую аналогией

Ну и зря. У вас не получилось. Ожидаемо.

это как если бы вы объясняли джаваскриптеру концепцию проверки типов C++, говоря, что предлагается просто писать

Я бы так не объяснял. А вот вы, вполне возможно. С тем же результатом, с которым вы здесь попытались “объяснить” мне подход с зависимыми типами.

Попробую еще раз донести до вас простую мысль: если вы хотите объяснить что-то собеседнику, то хорошо бы это делать на том уровне и на том языке, который понятен вашему собеседнику (а не вам лично).

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

Я вот даже не понял что за язык использован в вашем примере кода – Haskell, Agda или Idris. Соответственно, для меня нет ни вашего примера, ни вашего пояснения.

Код

index_validity element_at(std::size_t i, std::size_t N) {
  // нету проверки
  return valid_index { i - N / 2 };
}

тоже на непонятном вам языке?

Мой пример призван был всего лишь показать, что индекс, который программа может считать безопасным для использования, выражается отдельным типом (а не простым size_t или int-ом, как это сейчас массово встречается в реальном C++ном коде).

В написанном вами типе нет никаких указаний на то, является ли этот индекс на самом деле валидным для массива данной длины, и почему.

Типы не запрещают написать функцию, которая всегда для данных (i, N/2) возвращает valid_index { i - N/2 }. С завтипами такую функцию написать невозможно. Понимаете эту разницу? Или вы считаете, что это ерунда и эта разница несущественная?

Здесь нет никакого кода на непонятном вам языке, отсылок к теории, и так далее, это концептуальный вопрос. Сможете на него ответить?

Я бы так не объяснял.

Да, сорян, накосячил с позитивными и негативными позициями.

Это был бы ответ джаваскриптера вам, конечно. Он бы вас устроил, и вы бы согласились, что int перед age как отдельный сигнал компилятору — это несущественные мелочи и докапывание до столба, или таки нет, таки это ключевой момент?

Когда-то вам показалось, что потратить молодость на изучение С++ – это хорошая идея. Потом вам показалось, что приходить в комментарии к статьям про С++ и рассказывать какое C++ говно – это хорошая идея.

Ведь лучше — приходить в комменты об $X и воспевать $X, не так ли?

Теперь вам кажется, что в моих словах есть что-то воинственное.

Да, экстраполяция прошлого опыта: что опыта объяснений (когда более глупые, чем вы, люди умудрялись меня понимать), что опыта реакций на эти объяснения.

А если язык недостаточно выразителен, то это невозможно

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

Но приведенный вами пример кода на неизвестном мне языке оказался для меня совершенно бесполезным. О чем я вам и говорю.

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

А он и не может, т.к. это всего лишь незаконченный набросок.

тоже на непонятном вам языке?

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

Еще раз повторю для чего мне пришлось сделать свой набросок. Из вашего рассказа я понял (возможно, ошибочно), что зависимые типы заставляют разработчика сделать две вещи:

  1. Отказаться от использования в качестве индексов простых числовых типов (вроде int-ов, size_t и т.д.). Вместо этого вводится специальный тип, который обозначает “гарантированно валидный индекс”, при применении которого дополнительные проверки в run-time уже не нужны.

  2. Реализовать какой-то механизм порождения экземпляров этого типа.

Что я и попытался выразить примитивным и незавершенным наброском на C++. Там обе эти вещи выражены явным образом.

И я прекрасно понимаю, что это так себе приближение, но в рамках понятных мне вещей это максимум. Поэтому мне не интересно какие недостатки вы найдете в этом примере.

Типы не запрещают написать функцию, которая всегда для данных (i, N/2) возвращает valid_index { i - N/2 }. С завтипами такую функцию написать невозможно. Понимаете эту разницу?

Понимаю.

Или вы считаете, что это ерунда и эта разница несущественная?

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

Это был бы ответ джаваскриптера вам, конечно.

Здесь остается только спросить “а этот джаваскриптер, который типа мне что-то отвечает, он с вами сейчас в одной комнате?”

Он бы вас устроил, и вы бы согласились, что int перед age как отдельный сигнал компилятору — это несущественные мелочи и докапывание до столба, или таки нет, таки это ключевой момент?

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

Ведь лучше — приходить в комменты об $X и воспевать $X, не так ли?

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

Приходить ли в комменты про $X чтобы воспевать $X?

ХЗ, меня этот вопрос не заботит.

Да, экстраполяция прошлого опыта

Такая экстраполяция – это ваши личные предположения. Вы что-то себе предположили, а затем выдали это в качестве оценки для собеседника. Грубо говоря: “сами нафантазировали, сами побежали опровергать”.

Но тут можно было бы дать какое-то более понятное описание принципа работы зависимых типов либо просто в текстовом виде

В текстовом виде всё очень просто: типы могут зависеть от значений (поэтому они и зависимые). В привычных вам языках типы могут зависеть от других типов (примерно шаблоны классов), либо значения могут зависеть от типов (примерно шаблоны функций, ну или System F по-умному).

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

auto readIntsOrBools(std::string name)
{
  File file { name };
  using Type = file.read(1) == 'i' ? int : bool;
  int count = std::stoi(file.read(4));
  std::array<Type, count> result;
  // ...
  return result;
}

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

Доказательное программирование без (G)ADT’ов — отвратительно болезненная штука, а GADT’ов в плюсах нет, и эмулировать это на std::variant не стоит, лучше сделать вдоль.

либо в виде псевдокода на C++подобном языке (мол если бы в C++ было вот это и вот это, то…),

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

Это создаёт только иллюзию понимания.

Отказаться от использования в качестве индексов простых числовых типов (вроде int-ов, size_t и т.д.). Вместо этого вводится специальный тип, который обозначает “гарантированно валидный индекс”, при применении которого дополнительные проверки в run-time уже не нужны.

Для протокола — это не единственный возможный вариант, ещё можно использовать «обычные intы» и требовать доказательство (тоже в виде зависимых типов), что число меньше размера массива, но подход с отдельным типом более распространён (по не очень понятным мне причинам — экстернализация доказательств мне представляется более продуктивной по ряду причин).

И я прекрасно понимаю, что это так себе приближение, но в рамках понятных мне вещей это максимум. Поэтому мне не интересно какие недостатки вы найдете в этом примере.

Я считаю очень важным понимать границы применимости аналогий и иллюстративных примеров, но вы делайте как хотите, конечно. Моё дело — указать на эти границы.

Здесь остается только спросить “а этот джаваскриптер, который типа мне что-то отвечает, он с вами сейчас в одной комнате?”

Могу его покосплеить, легче станет?

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

Видимо, станет. Ну давайте.

Вы такой: вот как работают гарантии компилятора в плюсах: я могу написать int myFun(int age, string name, double height) и знать, что функция принимает значения таких-то типов в таком-то количестве и возвращает значение такого-то тип.
Джаваскриптер/я такой: а, ну это как в джаваскрипте я могу написать function iMyFun3(iAge, sName, dHeight) и тоже знать всё то же?

ХЗ, меня этот вопрос не заботит.

Однако, вы с этого вопроса начинаете комментарий и уделяете этак 20-25% его текста на попытки психоанализа собеседника.

Такая экстраполяция – это ваши личные предположения. Вы что-то себе предположили, а затем выдали это в качестве оценки для собеседника.

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

Кстати, после того, как я указал на это, вы стали пытаться разбираться глубже, так что не зря я на это указал!

Кстати, после того, как я указал на это, вы стали пытаться разбираться глубже, так что не зря я на это указал!

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

Подскажите, почему дизайн std::variant выглядит как говно, которое начали дизайнить, но бросили на полпути?

Вы бы хоть деталей каких дали. Как по мне, дизайн у std::variant отличный.

У нас больше нет страшных итераторов (какой v.begin(), v.end(), что это вообще?), 

А это ?- filter([](int n) { return n % 2 == 0; });

А что толку от этих новых стандартов, если весь написанный код написан мазохистами для мазохистов "по-дедовски", и с этим всё равно придётся иметь дело?

Нет времени сейчас:( почитаю потом

Напоминает отзыв на маркетплейсе: товар получил, но открою потом, оценка 5 ;-)

да, приблизительно

Хорошая и полезная статья. Спасибо.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации