В предыдущем видео мы с вами увидели, как даже в программе, написанной на идиоматически правильном современном C++, может быть неопределенное поведение. В этом видео мы рассмотрим еще один пример. Давайте рассмотрим следующую задачу. Пусть мы работаем в некотором графическом редакторе, допустим, в Powerpoint, и нам нужно получить примерно такую сетку из точек. Как это можно сделать? Например, у нас в начале дана одна точка. Мы эту точку выделяем, копируем, вставляем и помещаем рядом с текущей. Теперь у нас есть две точки. Мы эти две точки выделяем, копируем, вставляем, помещаем рядышком. Теперь у нас есть четыре точки. То же самое мы сделаем для четырех точек. И у нас появляется целая линия. Затем мы эту линию копируем, вставляем, помещаем рядышком и продолжаем процесс до тех пор, пока у нас не получается полная решетка, как мы и хотели. Кстати, очень классный способ, если понадобится, я рекомендую. Давайте его запрограммируем. Для этого воспользуемся уже знакомым нам сервисом. Определим класс точки. Пусть у нее будут целочисленные координаты x и y. И для удобства добавим конструктор. Передаем x, y. Инициализируем поля. Хорошо, теперь в нашей программе заведем вектор из точек vector points. И начнем с того, что положим на него только одну точку, собственно, с которой мы начинали. И для определенности, пусть она лежит в начале координат. Затем мы этот вектор как-то заполним и давайте выведем результат, points. Так, выведем координату x, затем координату y. Ставим запятую. Готово. Хорошо. Эту программу, в принципе, уже можно запустить и убедиться, что сейчас у нас есть вектор из одной точки. Давайте запустим. Да, все работает и видим, что в нашем векторе сейчас одна точка в начале координат. Давайте теперь выполнять эту операцию дублирования точек. Назовем это DuplicateAlong. Вначале мы дублировали вдоль оси x. Будем передавать вектор точек и то, насколько далеко нам нужно дублировать. Начнем с единички. То есть мы взяли исходную точку, передаем ее на единичку. Дальше у нас две точки. Две точки мы сдвинем на два. Получится четыре точки. Их мы сдвинем на четыре. Вот таким образом. И теперь давайте опишем функцию, которая будет это делать. Принимает она наш вектор из точек по ссылке и затем смещение, на которое нам нужно их сдвигать Получается, что для каждой входной точки мы должны сгенерировать еще одну точку со смещением. Давайте сделаем это вот так. Пробежимся по нашему вектору точек и для каждой точки мы будем вставлять еще одну: emplace back. Координату x мы сместим на offset, а координату y возьмем без изменений. Отлично. Вот так у нас будет выглядеть функция DuplicateAlong x. Давайте запустим нашу программу. Как мы видим, все прошло в точности, как мы ожидали. У нас есть несколько точек. Вдоль оси x они имеют координату от нуля до семи, и вдоль оси y у них ноль. Поскольку точек у нас сейчас будет побольше, давайте здесь перевод строки уберем. Дальше сделаем то же самое вдоль оси y. [БЕЗ_ЗВУКА] Собственно, функция у нас будет очень похожей, точно такой же сигнатуры, но теперь offset мы будем применять не к x, а к y. Отлично, давайте запустим теперь такую программу. Программа отработала и выводит нам что-то похожее на то, что мы бы и ожидали. Например, смотрите, тут у нас явно видно вторую строчку, где координата x возрастает от нуля до семи, а координата y равна единице. Как мы и ожидали. Но после этого в выводе появляются какие-то очень странные числа. Это явно какой-то мусор. И в соответствии с нашими формулами ему здесь взяться явно неоткуда. Если мы видим что-то подобное в результатах работы нашей программы, очень велика вероятность, что где-то у нас сюда закралось неопределенное поведение. И, действительно, в наших функциях DuplicateAlong x и DuplicateAlong y есть неопределенное поведение. Попробуйте подумать, в чем оно заключается, откуда оно берется. Как вы, возможно, догадались, проблема в этих функциях в том, что мы изменяем массив точек в то же время, как мы по нему итерируемся. Это, конечно, не очень хорошо. Для того чтобы точнее понять, что происходит, давайте взглянем, что под собой скрывает Range-based for. У нас есть страничка на cppreference, которая описывает Range-based for. И здесь мы видим, что Range-based for представляет собой вот такую конструкцию, в которой его раскрывает компилятор. То есть это на самом деле обычный цикл, но перед ним компилятор у диапазона, который передается в Range-based for, вытаскивает итератор на начало, сохраняет его, вытаскивает итератор на конец, сохраняет его. А потом инкрементирует итератор на начало до тех пор, пока он не дойдет до конца. Хорошо. То есть мы знаем, что у нас перед запуском этого цикла из массива будут считаны итератор на начало и конец. После этого мы делаем вектор emplace back. Как мы знаем, вектор emplace back, если он доходит до ситуации, когда ему некуда вставлять следующий элемент, он производит перелокацию, то есть выделяет новый буфер большего размера и переносит туда все элементы. Но при этом представьте, что произойдет, если эта перелокация случится в процессе работы данного цикла. Мы же уже посчитали, где находятся наши итераторы: наш begin и наш end. Они же останутся на тех же самых местах. То есть у нас наш вектор переедет по сути жить в новое место, а итераторы продолжат бежать по старому буферу. И этот старый буфер — это уже память, которая не используется. Это память, которая возвращена менеджеру памяти. И вообще говоря, в этом месте может появиться какой-то другой объект. Соответственно, использование таких итераторов — это неопределенное поведение. Хорошо. Но мы можем рассуждать следующим образом. Проблемы возникают в случае, если у нас в процессе работы цикла возникает перелокация. Не проблема. Хорошо. Давайте позаботимся о том, чтобы перелокации не возникало. А это делается с помощью метода reserve. Мы зарезервируем размер, который вдвое превосходит наш текущий. И тогда мы можем быть уверены, что перелокация у нас не произойдет. Отлично. Давайте запустим эту программу. Вот этот вывод уже очень похож на то, что нам нужно. Это ровно то, что нам нужно. Казалось бы, можно радоваться, но не совсем. Давайте для того чтобы понять, что теперь здесь не так, чуть-чуть внимательнее почитаем описание метода emplace back. Опять же, идем на cppreference. И здесь нам написано, если у нас новый размер превышает capacity, то тогда итераторы у нас не инвалидируются. Хорошо. Это то, с чем мы столкнулись изначально до того, как мы добавили метод reserve. Но, при этом итератор, который указывает после конца, то есть итератор, который возвращается с помощью метода end, всегда инвалидируется после выполнения метода emplace back. То есть это значит, что мы этот итератор получили до того, как мы первый раз вызвали emplace back, только в момент начала работы Range-based for, и он будет инвалидирован. И в сравнении с ним — это неопределенное поведение. Просто в данном случае данный компилятор не использовал это неопределенное поведение таким образом, чтобы результат программы стал каким-то неожиданным. Но в следующих версиях компилятора или в других компиляторах эта штука может выстрелить и результат работы программы будет совершенно не таким, как мы ожидаем. В связи с тем, что итератор, который возвращает метод end, всегда инвалидируется, на самом деле вряд ли здесь получится написать реализацию функций DuplicateAlong что-нибудь с использованием Range-based for. Здесь нужно использовать либо стандартные алгоритмы, либо обычные циклы, которые бегут по индексам. Итак, в этих двух видео мы с вами поняли, что использование идиоматического C++ помогает избежать проблем, связанных с неопределенным поведением. Но, к сожалению, не всегда. Бывают ситуации, даже когда этого оказывается недостаточно. Мы еще раз убедились, что неопределенное поведение действительно не определено и реально столкнулись с ситуацией, когда из-за неопределенного поведения нас атаковал велосираптор. Кроме того, мы поняли, что если программа работает правильно во всех всех компиляторах, всех версиях и со всеми настройками, которые мы только смогли перебрать, из этого еще нельзя делать вывод, что в ней нет неопределенного поведения, к сожалению. На самом деле, сложно сказать способ, который бы со стопроцентной уверенностью ответил бы нам на вопрос: в этой программе есть неопределенное поведение или его нет. Но с некоторыми практическими инструментами, которые умеют отвечать на этот вопрос, мы познакомимся в следующих видео.