Полное изображение ссылка0
Полное изображение ссылка0

— Как я стал таким программистом?
— Я был умнее самых умных и упорнее самых упорных!

перефразированная цитата Скрудж МакДака

Буду честен перед читателем. Все современные языки программирования обладают полнотой по Тьюрингу, то есть написанные алгоритмы будут эквивалентны между собой. Эквивалентность алгоритма означает одинаковый вход и выход. Так почему здесь указан язык C++?

Потому что автор знает его лучше, чем Python\Java\etc, но это утверждение верно лишь частично. На самом деле есть несколько причин: этот язык в силу своих особенностей может общаться напрямую с машиной, то есть компилироваться в машинный код. Множество других языков тоже умеют компилироваться в машинный код, возразит читатель. Верно, на это возражение есть еще несколько причин. Язык C++ не принадлежит никому, изначально он и разрабатывался в Bell Labs1, но как и язык C корпорация решила не лицензировать оба языка, в отличии от системы Unix. Тогда почему не язык C спросит проницательный читатель? Ответ на этот вопрос тоже есть причина. Цель создание языка C была переписать Unix систему на другую архитектурную платформу с минимум усилий, и эту возложенную функцию авторами язык выполнил на отлично. Изначально язык C++ был задуман для облегчения написания диссертации Бьерна Страуструпа2 для обработки сетевого многопоточного трафика. Академическая среда повлияла на концепцию и философию языка3. Первый десяток лет язык так и оставался забавной академической игрушкой, пока не произошел случай. Математик Александр Степанов после не совсем удачной попытки внедрить его теорию обобщенного программирования4 в язык ADA обратил свой взор на язык C++. По иронии судьбы внедрение обобщенного программирования в язык было реализовано даже не в половину от теории, которую задумал Степанов, так как использовались математические контракты и концепты. Они не были частью языка и не обрабатывались на уровне компилятора. Это и есть самая веская причина почему язык C++ подходит для информатики (компьютерные науки \ computer science), каждая новая версия языка (стандарт) привносит математическую строгость в язык и не теряет совместимость с наследием старой кодовой базы.

Я и так умею программировать

Исторически программисты появились не сразу, все это делали математики\физики\ученые, то есть составляли программу, которая автоматизирует математические расчеты. Эту историческую ретроспективу можно прочитать в статье Дейкстры5. С каждым поколением компьютеров расширялись его возможности, такими возможностями заинтересовались различные ведомства и корпорации. Появились задачи обработки документации, ведение учета. Примерно тогда нужно было обобщить категорию людей, которые составляют программы в профессию программист. Такие программисты все еще хорошо знали математику. Затем новые поколения компьютеров и их массовость, были достаточны надежными и дешевыми, чтобы за ними работали обычные работники предприятия, то есть выполняли свои задачи не используя языки программирования. Следующая итерация компьютеров достигла пика своего прогресса, теперь почти любой продвинутый пользователь компьютера, прочитавший несколько самых базовых концепций языков программирования таких как цикл, ветвление, вызов функции. Стали собирать из набора предоставленных модулей\функций свои программы. Понятие программист с каждым таким поколением выхолащивалось и превратилось в того, кто умело манипулирует «языковым конструктором». Поэтому часто в интернете спрашивают зачем программисту математика и другие наивные вопросы.

Информатика, зачем?

Термин информатика отделился от термина прикладной математики, потому что затрагивал автоматизированные вычисление математики, а также обработку физического мира - обработку различных сигналов. Многие авторитетные ученые все еще считают, что термин информатики (компьютерные науки \ computer science) избыточным. Здесь и далее математикой подразумеваются любые вычисление в том числе компьютерные, за исключением, где информатика подчеркивает контекст.

Рис. 1. «Гарвардские вычислители» за работой. Фото ок. 1890 года \ Harvard Computer6
Рис. 1. «Гарвардские вычислители» за работой. Фото ок. 1890 года \ Harvard Computer6

Компьютер (вычислитель) — это человек выполняющий математические расчеты (Рис. 1.), исторический термин. Почему люди делают механическую работу с подробной инструкцией? Ответ на этот вопрос переносит в древние времена. С античных времен Греции (возможно и раньше) человечество мечтало создать искусственного человека\автоматона, о чем свидетельствует древнегреческий миф о Талосе. Философы на протяжении всей истории человечества рассуждали каким должен быть искусственный мыслитель, параллельно многочисленные поколения математиков также хотели автоматизировать свой труд. Постепенное развитие наук и технологий. Дало возможность человечеству изготовить различные механизмы, сначала полу механические, а затем и полностью автоматические. Мечта человечества стала осуществляться появились, различные счетные устройства, такие как калькуляторы и автоматические картотеки. Через некоторое время был создан искусственный мозг - процессор, автоматический вычислитель - компьютер в современном его понимании.

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

  • Не хочет: тривиально, у вас есть некие математические формулы, которые требуют времени на вычисление и корректности результата. Человек может вычислить и сам, но эта рутинная операция, которая участвует в его научных исследованиях, пересчитывать коэффициенты каких-то свойств математики\физики. Вычисления такого рода отнимают значительную часть от исследовательской работы.

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

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

Примеры

Базовый пример:
Нужно умножить произвольное число на четыре. Тривиальная математическая формула — это алгебраическая переменная и константа 4:

a*4=

На языке процессора, в машинном коде, такая задача может иметь разную реализацию, на какой-то архитектуре это два сложения подряд (машина десятичная) переменной\регистра a. На другой это сдвиг регистра a на два бита влево (машина двоичная). Для читателя не знакомым с математикой может показаться что это причуда инженера, который проектировал процессор, но на самом деле это свойство математики, вспомните что, когда вы умножаете или делите на 10 двигаете запятую влево или вправо на один разряд. Точно также в двоичном представлении двигается разряд запятой. Читатель может возразить что в целых числах нет запятой. На самом деле есть, и вот почему. Рассмотрим задачу: беззнаковое целочисленное деление на 3 всегда можно реализовать умножением7. В математике с вещественными числами это выглядит как a*0,(3). Но как поступить, когда у нас нет вещественных чисел. Нужно просто заранее знать коэффициент(точность) умножения и последующим сдвигом влево на количество значащих знаков в коэффициенте. Выглядит сложноватым, но на языке компьютера это выглядит как:

unsigned int a=43;//тут в качестве примера выбрано число 43
unsigned int b=0b01010101;//коэфф. деление на 3 с точностью 8 бит по формуле (2^8+1)/3
a=(a*b)>>8;//переменной\регистре получим ответ 14 что и требовалось

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

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

Сложный пример:

Алгоритм — совокупность математических действий, решающая конкретную математическую задачу. Обычно программа — это совокупность алгоритмов. Например, рассчитать первые 100 простых чисел и вывести результат на экран. Первый алгоритм можно реализовать с помощью решета Эратосфена, а вот выводит на экран уже сложнейшая математическая задача, по сути, это синтез символов в реальном времени, так как компьютер хранит символы в виде зашифрованных числовых значений. Такую сложную задачу мы рассматривать не будем, так как она может занять не один том, если рассматривать различные приемы и тактики вывода символа, а также архитектуру экрана.

Поговорим о математическом анализе сложности алгоритмов. Хорошим классическим примером будет рассмотреть несколько алгоритмов сортировки.

Сортировка — это операция над некоторой последовательностью чисел(объектов), которая переставляет последовательность так, что числа в последовательности идут в порядке возрастания от малого к большему. Рассмотрим два алгоритма: Сортировка пузырьком \ Bubble sort8, Сортировка слиянием \ Merge sort9. (это два итеративных алгоритма, но детали реализации, нас не интересуют)

Сортировка пузырьком \ Bubble sort

void bubbleSort(int a[], int l) 
{ 
    int i;
    int j;
    for (i = 0; i < l - 1; i++) 
        for (j = 0; j < l - i - 1; j++) 
            if (a[j] > a[j + 1]) 
                swap(a[j], a[j + 1]); 
} 

Сортировка слиянием \ Merge sort

void MergeSort(int a[], int l)
{
    int BlockSizeIterator;
    int BlockIterator;
    int LeftBlockIterator;
    int RightBlockIterator;
    int MergeIterator;
    
    int LeftBorder;
    int MidBorder;
    int RightBorder;
    for (BlockSizeIterator = 1; BlockSizeIterator < l; BlockSizeIterator *= 2)
    {
        for (BlockIterator = 0; BlockIterator < l - BlockSizeIterator; BlockIterator += 2 * BlockSizeIterator)
        {
            LeftBlockIterator = 0;
            RightBlockIterator = 0;
            LeftBorder = BlockIterator;
            MidBorder = BlockIterator + BlockSizeIterator;
            RightBorder = BlockIterator + 2 * BlockSizeIterator;
            RightBorder = (RightBorder < l) ? RightBorder : l;
            int* SortedBlock = new int[RightBorder - LeftBorder];
            while (LeftBorder + LeftBlockIterator < MidBorder && MidBorder + RightBlockIterator < RightBorder)
            {
                if (a[LeftBorder + LeftBlockIterator] < a[MidBorder + RightBlockIterator])
                {
                    SortedBlock[LeftBlockIterator + RightBlockIterator] = a[LeftBorder + LeftBlockIterator];
                    LeftBlockIterator += 1;
                }
                else
                {
                    SortedBlock[LeftBlockIterator + RightBlockIterator] = a[MidBorder + RightBlockIterator];
                    RightBlockIterator += 1;
                }
            }
            while (LeftBorder + LeftBlockIterator < MidBorder)
            {
                SortedBlock[LeftBlockIterator + RightBlockIterator] = a[LeftBorder + LeftBlockIterator];
                LeftBlockIterator += 1;
            }
            while (MidBorder + RightBlockIterator < RightBorder)
            {
                SortedBlock[LeftBlockIterator + RightBlockIterator] = a[MidBorder + RightBlockIterator];
                RightBlockIterator += 1;
            }
            
            for (MergeIterator = 0; MergeIterator < LeftBlockIterator + RightBlockIterator; MergeIterator++)
            {
                a[LeftBorder + MergeIterator] = SortedBlock[MergeIterator];
            }
            delete [] SortedBlock;
        }
    }
}

Не подготовленному читателю может показаться что алгоритм сортировка пузырьком будет работать быстрее чем сортировка слиянием, потому что строчек кода в первом алгоритме меньше. Но на самом деле все с точности на оборот, и количество строчек кода — это не метод оценки производительности алгоритма. Распространенное заблуждение в программировании (см. выше раздел Я и так умею программировать). Здесь нужен математический анализ. В информатике такой анализ алгоритмов называется «O» большое10.

Не будем вдаваться в тонкости доказательств, и примитивно оценим наше «O». Как правило сложность алгоритма заключается в начальной последовательности элементов, то есть вход с количеством n. Таким образом пройти все элементы алгоритмом один раз это O(n), а если алгоритм принимает на вход последовательность и не проходит ее хотя бы один раз то это O(1). Таким образом в алгоритме сортировка пузырьком, два цикла один внутри другого мы проходим последовательность n раз по n раз получается O(n2). В алгоритме один главный цикл это n проходов, и еще есть подциклы в совокупности они дают log n. Не будем вдаваться в подробности алгоритма, сравним их по нотации большое О. O(n2) > O(n log n) соответственно где больше, тот алгоритм и дольше вычисляется. При этом с точки зрения результата он эквивалентен. При подсчете большое О можно легко ошибиться, если вы взываете, какую-то функцию внутри цикла. Как функция использует вашу последовательность остается либо гадать, либо читать документацию к этой функции. Поэтому многие языки программирования с точки зрения математика не гарантируют ровным счетом ничего.

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

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

  1. Иллюстрация для титульной страницы. https://habr.com/ru/articles/147762/

  1. Книга: Время UNIX. A History and a Memoir \ UNIX, A History and a Memoir Автор: Брайан Уилсон Керниган \ Brian Wilson Kernighan.

  2. Книга: Дизайн и эволюция языка C++ \ The Design and Evolution of C++, Автор: Бьерн Страуструп \ Bjarne Stroustrup.

  3. Раздел Филосовия С++ https://ru.wikipedia.org/wiki/C%2B%2B#Философия_C++ \ https://en.wikipedia.org/wiki/C%2B%2B#Philosophy

  4. Книга: От математики к обобщенному программированию \ From Mathematics to Generic Programming, Автор: Александр Степанов, Дэниэл Э. Роуз \ Alexander A. Stepanov, Daniel E. Rose.

  5. Статья: Смиренный программист EWD340 \ The Humble Programmer (EWD 340) автор: Эдсгер Вибе Дейкстра \ Edsger W. Dijkstra

  6. Фото «Гарвардские вычислители»: https://ru.wikipedia.org/wiki/Гарвардские_вычислители \ https://en.wikipedia.org/wiki/Harvard_Computers

  7. Книга: MMIX RISC-компьютер для нового тысячелетия \ MMIX a RISC Computer for the New Millennium, автор: Дональд Эрвин Кнут \ Donald Ervin Knuth. Глава 1.3.1 Задание 17 [M22].

  8. https://ru.wikipedia.org/wiki/Сортировка_пузырьком \ https://en.wikipedia.org/wiki/Bubble_sort

  9. https://ru.wikipedia.org/wiki/Сортировка_слиянием \ https://en.wikipedia.org/wiki/Merge_sort

  10. https://ru.wikipedia.org/wiki/«O»_большое_и_«o»_малое \ https://en.wikipedia.org/wiki/Big_O_notation

Комментарии (26)


  1. atd
    15.01.2024 05:45
    +6

    ...Так почему здесь указан язык C++?

    Потому что автор знает его лучше...

        int* SortedBlock = new int[RightBorder - LeftBorder];
        ...
        delete SortedBlock;

    <mic drop> (простите за токсичность)

    P.S.: за саму статью плюс, чувтсвуется личное мнение, а не очередной пересказ


    1. Boctopr Автор
      15.01.2024 05:45
      +3

      Моя вина, плохо проверил копипасту с википедии (исправлено), но внимательный читатель должен заметить, алгоритм полностью скопирован с википедии. Очередные миллионы глаз на опенсорсе. Википедии такой код лежит с 12:13, 31 мая 2016


      1. boldape
        15.01.2024 05:45
        +4

        Так давно не писал сырой дэлет, что целую минуту тупил в чем проблема.


      1. isadora-6th
        15.01.2024 05:45

        Используйте std::vector в 100% случаев когда планируете написать new/delete пару и будет вам счастье.

        Ну а про то, что зачастую в компиляторах по умолчанию включено расширение из мира C, VAL, можно в целом было написать:

        int SortedBlock[RightBorder - LeftBorder];

        Но вариант с std::vector все еще предпочтительней.

        Ну и сортировка это std::sort, пишите на современном языке, а не по гайдам дедов из 2002.


        1. wataru
          15.01.2024 05:45

          VAL, конечно, можно написать, но заводить большой массив на стеке - далеко не лучший вариант. Можно и stack overflow заработать.

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


          1. isadora-6th
            15.01.2024 05:45
            +1

            Размер стека зависит от платформы, но даже 1Мб это так то 250тыс интов... Но так то да.

            Ответы про размер стека которые я по быстрому нагуглил, говорят о том, что это это зависит от платформы и на линуксах около 7.4Мб, но зависит от окружения и лимит можно вообще снять... На винде в 2012 был 1Мб. Можно указать при сборке.

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

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

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


        1. AnSa8x
          15.01.2024 05:45
          -2

          Совет из разряда "я у мамы погромист".
          Зачем мне полный функционал std::vector если я не планирую использовать даже половину из него?
          Зачем мне вообще эта тяжелая зависимость в виде вектора если мне просто нужен кусок динамически выделенной памяти?
          Возможно это полезный совет для тех кто только начал изучать язык, но плохой для всех остальных.
          А если кто-то принципиально или по каким-то другим причинам не использует исключения? Как быть?
          В случае с VLA я думаю вы всё же понимаете чем он отличается от динамического выделения памяти.
          Я бы даже сказал что это совет для очень невнимательных людей, которые либо забывают освобождать память, либо освобождают неправильно.


          1. isadora-6th
            15.01.2024 05:45
            +4

            Зачем мне полный функционал std::vector если я не планирую использовать даже половину из него?

            Так вы на Си пишете) Вектор нужен для автоматического менеджмента памяти, никто вас не заставит его использовать на полную. Можете вообще в C-style через квадратные скобки писать. Компиляторы неиспользуемый код вырезают из итоговой сборки. Минусы не вижу. Если знаете желаемый размер, можете делать .reserve(). Вызовы методов свернутся компилятором на оптимизации выше -O0.

            Если у вас к квартире подведен водопровод, то как вы смеете использовать кран не только в верхнем положении!!!

            Зачем мне вообще эта тяжелая зависимость в виде вектора

            Она прекрасна как минимум тем, что автоматически удалит кусок памяти в коде в котором вы делаете early return, без любого риска утечки этой самой памяти. Работал я над одним проектом, где так экономили применение векторов, что мы очень часто обнаруживали "забытый" free/delete в какой-то длинной цепочке логики) А как часто был use after free, а повторные очищения, мм закачаешься. Но там легаси с хедерами из 92 года, не делайте так в современном мире!

            Ну и про тяжелую, тут сложно сказать, как вектор вас ощутимо затормозит. Оптимизация времени сборки это откровенно говоря не с вектора начинается, а с того, что у вас в хедерах кросс инклюды 90% проекта. Выиграл я как-то 30% времени сборки заменив в "util.h" подключенного всей кодовой базой объявления на форварды спилив подключения.

            А если кто-то принципиально или по каким-то другим причинам не использует исключения?

            В среднем, не умеют готовить. Если ваша кодовая база, не содержит RAII примитивов, которые на деструкторе почистятся - эксепшен прямой путь к утечке для экономистов на векторах. Кодстайл где вы руками трогаете память не позволит их внедрить. Про перформанс который убивают эксепшены реально забавно, ведь если вы их не бросаете то оверхеда то толком нет... (Ловушка на известный видос, где человек показывал LLVM байткод кода с эксепшенами)

            В случае с VLA я думаю вы всё же понимаете чем он отличается от динамического выделения памяти.

            Ограниченный лайфтайм = нельзя возвращать. Ну и случаи когда вам куска в 7 мегабайт мало будет. Основная проблема в том, что новый компилятор которым вы будете собирать проект, может это отказываться прожевывать.

            забывают освобождать память

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

            Я уверен, что концепция std::shared_ptr вас вообще в ужас вводит. Прекратите писать на Си и начните уже писать хотя бы на C++11. (13 лет прошло).

            Если есть консерн по перформансу - меряйте, люди крайне плохи в оценке перформанса на глаз.


            1. AnSa8x
              15.01.2024 05:45

              Так вы на Си пишете) Вектор нужен для автоматического менджмента памяти, никто вас не заставит его использовать на полную. Можете вообще в C-style через квадратные скобки писать. Компиляторы неиспользуемый код вырезают из итоговой сборки, минусы не вижу.

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

              "Зачем мне вообще эта тяжелая зависимость в виде вектора"
              Она прекрасна как минимум тем, что автоматически удалит кусок памяти в коде в котором вы делаете early return...

              Кто что удалит, простите?

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

              Меня вектор не тормозит. Я им не пользуюсь. Но он может заметно "затормозить" время сборки проекта. Всё зависит от того на чём вы собираете и как часто подключаете этот заголовок, собственно как и любой другой из STL.

              Оптимизация времени сборки это откровенно говоря не с вектора начинается, а с того, что у вас в хедерах кросс инклюды 90% проекта.

              Да откуда вы вообще берёте такую информацию?
              Про то что всем нужно использовать вектор, про то что у меня в проекте 90% "кросс инклуды". Мне кажется вы слишком высокого о себе мнения.

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

              А если содержит? Значит и вектор не нужен получается!?

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

              Внедрить что? "RAII примитивы"? Запросто.

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

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


              1. isadora-6th
                15.01.2024 05:45
                +3

                Кто что удалит, простите?

                std::vector удалит (освободит) выделенную память.

                Но он может заметно "затормозить" время сборки проекта.

                Как вы сделали этот вывод? Включение инклуда и подстановка типов на темплейт это конечно тяжелые задачи, но "затормозить", вы же не про 131мс на чистой сборке?

                > Оптимизация времени сборки это откровенно говоря не с вектора начинается, а с того, что у вас в хедерах кросс инклюды 90% проекта.

                Да откуда вы вообще берёте такую информацию? ... про то что у меня ...

                Я об абстрактном проекте. Если скорость сборки это вопрос, то обычно чинят что-то более значительное чем "тяжелый" вектор.

                А если содержит? Значит и вектор не нужен получается!?

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

                > Кодстайл где вы руками трогаете память не позволит их внедрить.

                Внедрить что? "RAII примитивы"? Запросто.

                Внедрить exceptions, просто потому-что начинаются exception guarantee, и желательно отсутствие утечек. new/delete пара не дает никаких гарантий на вылете эксепшена между ними.

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

                В среднем, те, кто рассказывает про то, что вектор тяжелая зависимость - говорят, что медленно и вообще эти ваши классы медленно, не то что маллоки. А потом мантры про "тормозные эксепшены". Приятно знать, что вы не такой.

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


                1. AnSa8x
                  15.01.2024 05:45

                  Наш диалог выливается в пустую болтовню, которая никак не подтверждает полезность слов "Используйте std::vector в 100% случаев когда планируете написать new/delete пару и будет вам счастье."
                  Весь ваш постулат звучит так: если вы тупой, невнимательный, ленивый или просто не в состоянии создать свой контейнер с нужным функционалом - используй вектор.
                  И да, интересный подход (если кто знает, скажите как это называется) в диалоге - сказать что один не хотел задевать/обидеть другого). Я даже не знаю что вам на это ответить, ей богу...


                  1. boldape
                    15.01.2024 05:45
                    +1

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

                    Ну и про ВАЛ его в с++ нет, никогда не было и нутром чую никогда не будет, ВАЛ как и структуры переменного размера я видел только в С.


                    1. isadora-6th
                      15.01.2024 05:45

                      Вы в таком подходе еще и basic exception guarantee (которая, про то, что нет утечек и объекты в валидном состоянии) получаете, что вообще прекрасно. Насколько я знаю, вызовы методов в векторах обычно сворачиваются (на -O0 не сворачиваются, поэтому их не любят в геймдеве), но реальный эффект на перформанс от векторов против массивов не мерял, дайте линк если есть.

                      Всю беседу я считал, что AnSa8x из староверов и вообще std не любит и предлагает подход на new/delete, ведь он достаточно умен, что-бы совладать с этим.

                      AnSa8x: Я бы даже сказал что это совет для очень невнимательных людей, которые либо забывают освобождать память, либо освобождают неправильно.

                      Поэтому тут тейк не столько про вектор, сколько про "не трогайте память руками и будет вам счастье". Вектор просто это все дает из коробки.


                      1. AnSa8x
                        15.01.2024 05:45

                        Вы в таком подходе еще и basic exception guarantee (которая, про то, что нет утечек и объекты в валидном состоянии) получаете, что вообще прекрасно.

                        Откройте для себя std::nothrow. Чего вы вообще к этим исключениям привязались? У вас без них жизни нет что ли?


                      1. isadora-6th
                        15.01.2024 05:45
                        +1

                        Да даже и без них, просто написать вектор, вкинуть в него случайное число значений, вернуть из функции просто. А потом никакого головняка с тем, что точки вызова функции правильно почистили delete[] чужую память, а если у них есть вызывающий код, то они должны корректно чистить и выше... тоже...

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

                        Без векторов:

                        struct Order {
                          std::string order_id;
                          OrderItem* items; 
                          int64_t items_count;
                        };

                        Если городить new/delete[] в конструктор-деструкторах то вы просто велосипедите std::vector и получаете необходимость руками мять память в местах где это не очень нужно, а также головняк на копированиях этих конструкций (если копировать указатель, деструктор любого экземпляра почистит память).

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

                        Поэтому вектора очень важный удобный инструмент и мучать память руками это моветон.

                        Я бы даже сказал что это совет для очень невнимательных людей, которые либо забывают освобождать память, либо освобождают неправильно

                        Можно не выделять (руками), чтобы не освобождать (руками). Оверхедов (относительно автоматики) тут не много.

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


                      1. boldape
                        15.01.2024 05:45
                        +1

                        Ну вектор не панацея от управления памятью и даже иногда вредит так как тоже предоставляет возможность проиграть в той самой игре на внимательность. Ещё не догадались о чем речь?

                        Вставка в вектор ИНОГДА, и это ключевая беда, инвалидирует итераторы. Классические грабли на которые я наступал и не раз и даже после 15 лет это цикл по вектору в котором вставляем в него.

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

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

                        Не так страшен сырой нью как сырой дэлет, но страшнее всех деструктор.


                      1. AnSa8x
                        15.01.2024 05:45
                        +2

                        Я вот тоже не понимаю почему иногда довольно простые вещи у людей головняк вызывают.
                        Велосипеды далеко не всегда изобретают от безделья, иногда для этого есть и причины. У кого-то память ограничена, кто-то пытается выжать максимальную производительность, кого-то функционал не устраивает.
                        Но тут каждому своё. Поэтому я и не говорю: всегда используйте new/delete. Если у кого-то нет причин для ручного выделения памяти или это вызывает головняк - да ради бога, пользуйтесь вектором.
                        Но вот лично для меня он неудобен (вы же не думаете что я один такой?), поэтому ваш совет про всегда и везде для некоторых может звучать как навязывание того чего им не нужно.

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

                        Мне тоже не очень понятно что вы имеете в виду под "массив struct-ов (неизвестной длинны на компайл тайме)".


            1. AnSa8x
              15.01.2024 05:45
              +1

              Я уверен, что концепция std::shared_ptr вас вообще в ужас вводит.

              Не знаю кто вас в этом уверил. Очень удобная концепция. С удовольствием пользуюсь.


        1. geher
          15.01.2024 05:45

          Используйте std::vector в 100% случаев когда планируете написать new/delete пару и будет вам счастье.

          Почему не std::array?


          1. isadora-6th
            15.01.2024 05:45

            std::array is a container that encapsulates fixed size arrays.

            Переменной его размер не задать - только константой (в данном случае параметром шаблона). Когда зовут new/delete обычно размер выделяемого блока динамический.


            1. geher
              15.01.2024 05:45

              Так раз на два не приходится.

              Иногда (и не так уж и редко) размер как раз фиксированный (и слишком большой, чтобы на стеке обычным массивом держать). Так что 100% твки многовато будет, и иногда array очень даже сгодится. Конечно, раньше как-то жили без него, но зачем-то его таки сделали.


              1. isadora-6th
                15.01.2024 05:45

                std::array<T, size_t> выделяется на стеке если не делать обертки.

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

                Хорошо обернутый std::array который хранится на хипе это собственно говоря и есть вектор.

                Сделали std::array для правильного хранения размера на компайлтайме (а не в переменной рядом с ним) и вообще в stl стиле. Хорошая нишевая штука, но очень не для всех случаев.


    1. zagayevskiy
      15.01.2024 05:45

      Так обычно и бывает. "Знаю С++", а в коде треш и угар. И дело даже не в забытых скобочках, а в том, что так уже много лет никто не пишет.


      1. AnSa8x
        15.01.2024 05:45

        Позвольте поинтересоваться. Так - это как?


        1. zagayevskiy
          15.01.2024 05:45

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


  1. Hivemaster
    15.01.2024 05:45

    На сколько я понял, автор - настоящий программист, знающий математику. Что уже изобрели? Какие алгоритмы разработали? Чем из ваших творений, которые смог бы разработать ненастоящий программист, восторженно пользуется мир?