Pull to refresh

Comments 277

Жуть конечно, но это в порядке вещей… Я когда заказывал сайт на фрилансе такое тоже встречал)

Для меня правило хорошего тона всегда брать в скобки и выносить на отдельную строку/в отдельную переменную то, что вызывает повышенное шевеление извилинами в во всём остальном обычном коде. Не вижу никакого смысла постоянно напрягать память для того, чтобы вспомнить каким именно образом компилятор или интерпретатор обрабатывает тот или иной уникальный случай. Проще написать больше тупого кода, чем меньше излишне умного. Сложно придумать аргументацию против такого подхода.


Хотя в последнее время читая код весьма продвинутых программистов вижу на удивление большое количество без необходимости сокращенных названий переменных, невыразительных названий функций/методов и полное отсутствие малейшего комментирования происходящего. Может это я такой тупой, но всё же не вижу никакого смысла в сокращении privateKey до pk и прочих подобных, ибо код превращается в кроссворд из кучи переменных каждая из которых содержит максимум 3 символа, а чаще вообще 1.

UFO just landed and posted this here

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

я работаю сейчас с географическими/прямоугольными координатами. Там х и у наоборот. И часть библиотек написано математиками(х вправо, у вверх), а часть геодезистами(наоборот). Боюсь, мне захочется назвать их сложнее :))
А z там есть, но спрятан.
О как это знакомо! Вероятно это будет fi и la (φ и λ) или lat и lon.
Вероятно это будет fi и la (φ и λ) или lat и lon.
А почему не φ и λ?
Можно и φ и λ, если живёте где-нибудь в Афинах и у вас на клавиатуре есть эти символы. Правда я думаю, что это будет равносильно написанию названий переменных на русском у нас.
Если мы говорим о мейнстриме (C++/C#/Java), то общая практика состоит в избегании применения не ASCII символов в коде. До сих пор бывают проблемы с кодировкой исходников, что приводит к нежелательным последствиям. А ещё можно представить, что потом эти названия полезут в некое публичное API. А это накладывает дополнительное требование поддержки не ASCII-символов на все инструменты, которые с этим API работают, будь это линкеры для библиотек или OData/REST кодогенераторы для WebApi. Слишком большая проблема для ровного места :)
Забавный пример из личной практики, связанный с именами переменных — когда в конце 80-х я осваивал программирование на МК-61 и МК-52, то одной из моих любимых книжек была книжка небольшого формата Очкова и Хмелюка «От микрокалькулятора к персональному компьютеру». Там были примеры программ как для микрокалькуляторов, так и на Бейсике. Меня в то время долго мучал вопрос — почему во многих программах фигурирует переменная с загадочным именем TEMP, никак не связанная с температурой ))) (cитуация усугублялась тем, что в школе я учил немецкий).

zeitweilig куда как более популярное прилагательное в немецком.

i, j и k это зло. В циклах должны быть нормальные переменные типа rowIndex, columnIndex и т.п. Два вложенных фора c i/j очень часто на этапе написания содержат ошибку. И даже если ошибки нет, время потраченное на осмысление можно использовать более продуктивно.
Два вложенных фора c i/j очень часто на этапе написания содержат ошибку.

Прокладку, между стулом и клавиатурой (=
Значение этих переменных очевидно настолько, что замена мнемониками пользы не принесет. Есть такое правило: чем меньше скоуп жизни переменной, тем короче имена. Если в таком цикле возможно даже i с j перепутать, то тут уже стоит подумать о том, как его отрефакторить.

На самом деле, стоит задуматься о шрифтах в любимом редакторе (или IDE).

А я согласен с TerraV.

Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c, а не a, b, c, d (и не i, j, k, l). Если одной буквы не хватает для уникальной идентификации (например, columns и cells, или tests и testTargets) — двумя или даже полными словами.
Тут действительно большая разница между i,j,k и хитромудрыми a,b,c,d, о семантике которых любой бы задумался. for и i — это как хлеб и рама.
Вы, наверно, имели в виду «хитромудрыми a, u, r, c».
Их тоже, это не имеет значения на самом деле) Если в контексте имя переменной не дает стойкой ассоциации (как, например, r в геометрической формуле), то лучше воздержаться.

Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c

Использование i,j,k в качестве индексаторов — это общепринятая практика, а однобуквенные штуки намекающие на тип будут уместнее в foreach-циклах, где это не просто индекс, а экземпляр, обладающий состоянием и поведением.
Экземпляр, обладающий состоянием и поведением? Похоже на унижение школьника учителем.
Нет, это не пикабу, здесь сидят специалисты, владеющие терминологией.
Этой терминологии всего несколько десятков лет, она вполне может поменяться. Меня же коробит что используют неочевидные языковые конструкции, наталкивающие на неправильные мысли. Я бы написал так: это не просто индекс, а элемент структуры данных. Статья именно об этом — о неочевидности кода.
Фундаментальные понятия имеют интересное свойство: меняться либо редко, либо никогда.

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

Любая наука обрастает своей профессиональной терминологией, что поделать?.. Или вы предлагаете «хреновинами» и «фиговинами» оперировать? (=
Если для вас это неочевидно, то это печальненько, ибо рассказывают это студентам на первом курсе.

Я бы написал так: это не просто индекс, а элемент структуры данных.

Сложными словами я как раз подчеркивал, что индекс != элементу коллекции, это всего лишь подярковый номер, без «состояния и поведения».
И? В чём разница?

А, я понял. Вы вспомнили про ту глупую традицию называть параметры шаблонов T, T1, T2, …? Ну так это тоже нафиг.

Параметры шаблонов должны иметь осмысленные имена. Стилизания их названий должна быть такая же, как и стилизация того, что они обозначают (если параметр-тип и типы мы пишем с большой буквы, то с большой; если параметр-константа и константы мы пишем так-то, то так-то). MySuperCollection<Item> (или на худой конец MySuperCollection<ItemType> — хотя это спорно, мы ведь называем Integer и CustomerInfo, а не IntegerType и CustomerInfoType — ну да ладно), но не MySuperCollection<T>.
И? В чём разница?

Есть некая someFunction<TInput, TOutput>, где в качестве TInput и TOutput может быть ну совершенно что угодно. Какие буквы вместо i и j вы предложите для переменной-индекса?

А вот этого я не понимаю. Ладно i/j/k означают не более чем «число от 0 до x», но зачем сокращать итераторы?

Другое дело — функции, делающие что-то тривиальное, когда кроме «v1», «v2» сложно выдумать что-то подходящее. «firstValue/secondValue»?
А в чём принципиальная разница между целочисленными счётчиками цикла и итераторами — не понимаю.
Огромная. В том, что здесь мы имеем дело не с абстрактным «числом от одного до десяти», а с конкретным элементом конкретного списка. И логично элемент списка «addresses» назвать «address», «users» — «user» и т.д.
Итератор — это, как правило, объект, позволяющий получить доступ к ячейке контейнера. Например, итератор позволяет вам перемещаться между узлами связного списка, где инкремент счетчика бесполезен.

Дело в том, что целочисленные индексы используются всегда совместно с коллекцией: users[i]. При этом вся необходимая для понимания семантики информация уже сосредоточена в имени коллекции.


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

Зло, нужно for-ы по возможности заменять foreach-ами.
Зря минусите человека.

i,j,k — вполне применимы в простых случаях без вложенных циклов, хотя и не помню когда последний раз их использовал, т.к. легко заменяются на foreach.
А вот при работе с таблицами я бы предпочёл видеть rowIdx и colIdx — чуть больше писать, зато позволяет избегать детских, но труднонаходимых ошибок в коде.
Не так давно разбирался в одноразовой функции getAndConvertPhysicalToLogicalValue и нихрена не понял откуда она и что берёт. В результате функция вырезана нафиг, а логика работы (то, что функция должна была делать) перенесена в тело цикла. При этом пропала передача параметров и прочие сопутствующие причандалы.

Где-то есть золотая середина.
нихрена не понял откуда она и что берёт
Т.е. функция была сложна и непонятна?
логика работы (то, что функция должна была делать) перенесена в тело цикла
Т.е. тело цикла и функции, в котором он находится, стало ещё более сложным и непонятным? А в чём профит Вашего действия?
Функция была странная, а термины «Logical» и «Physical» в этом контексте никогда не употреблялись.
Тело цикла стало больше, но в силу того, что параметры никуда не передаются можно отследить логику работы.

Переименовать не пробовали?)

UFO just landed and posted this here
Согласен на 100%.
Функция вызывалась один раз, называлась длинно с использованием непонятных терминов, передавала всякое туда-сюда по значению.
Заменилась на три строки в теле цикла.

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


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


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


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

Комментарии в чужом коде — большая моя боль. Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?


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

Зависит от того, как ещё переменные и методы обозваны. При хорошему подбору названий необходимость в комментариях резко уменьшается.

Главный критерий достаточности количества комментариев — чтобы сторонний человек не реагировал как Alex_ME (собственно я так же реагирую)
Вот сейчас это был очень популистский камент :)

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

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

Я работал с программистом, который делал метод, к примеру, «FindMemo», и оставлял комментарий: «Файндит мемо». Несмотря на то, что добавить ему было нечего, понятней этот кусок кода не становился.

Так о том и речь — что добавление комментария не добавляет смысла. Если метод назван понятно — то и комментарий не нужен. Если метод назван непонятно — то и комментарий будет таким же непонятным :-)

Дак я не о том =)

Опять же, если взять мой пример с «FindMemo», рядом есть дефолтный метод Find. Для чего нужно было делать кастомный, и что такое memo в текущем контексте можно узнать только если детально разобраться в методе. Эту информацию и нужно было оставить в комментарии

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


Чтобы написать правильный комментарий, нужно было чтобы программист осознал что "Файндит мемо" — непонятно. Но в таком случае что помешало бы ему и метод тоже назвать по-другому?

Обычно надо не метод переименовывать, а подробней описать аргументы и результат (ну, как обычно описаны функции у ms/apple/..., и в doxygen/… удобно делать)

То, что в этом случае метод имел бы имя длиной символов в 50. Для комментария-то это нормально…
И для функции — тоже нормально, если короче понятное название не придумывается. Важно же, чтобы в том месте, где функция вызывается было понятно что она делает — а там вашего чудного комментария уже и не будет.
На помощь приходит ide, которая в подсказке указывает описание метода и параметров
Вы читаете код, тыкая в каждую функцию и читая описание метода и параметров? Мне вас жаль.

На практике код приходится читать гораздо чаще, чем писать, так что важно оптимизировать именно скорость чтения кода.
Сталкивался со случаями, когда мне говорили: «Непонятное название — укороти.»
Категорически не согласен с таким — слишком длинное (и написанное на грамотном английском) название может быть неудобным в использовании или ещё что-то, но не непонятным.
Жаль, что существующие языки/IDE не предлагают вменяемых механизмов сокращения названий функций (типа, вообще она называется длинно и в достаточно удалённых модулях её будут называть полностью, но в нашем модуле, имеющем с этим набором функций дело часто, мы будем называть их так-то, так-то и так-то, а не полностью).
use function cos as c;
echo c(0);  // 1

var getById = document.getElementById;
console.log(getById);  // function getElementById() { [native code] }
Надо смотреть по каждому конкретному случаю. Если имя функции такое длинное, оно не будет влазить с параметрами в строку.
Методы не висят в воздухе, а лежат внутри типов (инстансы которых также могут иметь имена), реализующих интерфейсы, которые находятся в пространствах имен. Всё это уточняет контекст.
А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP. (Было бы неплохо посмотреть на реальный пример)
Ок, пусть это не метод, а просто функция.

А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP.

Допустим, некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.
Имена свободных функций обычно длиннее. Ну ОК, это не сильно меняет суть.

некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.

Нужен какой-то хороший пример. Что за айтем, откуда мы его берем? Если добавится еще 2-3 слова, то ничего страшного в этом нет. В названии нужно описывать цели, а не алгоритмы.
UFO just landed and posted this here
… и будет нифига непонятно, как они работают в целом) Надо все-таки соблюдать баланс.
UFO just landed and posted this here

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

Есть такая штука: разбиение метода на два и более.
Если вам хочется назвать метод более чем 3-мя словами, то либо у вас словесный понос, либо ваш метод делает слишком много одновременно и нарушает хотя бы принцип единственной ответственности (single responsibility).

Не в этом дело.

Это редкие случаи, но вполне реальные. Я сейчас не буду искать по коду эти редкие случаи, вот синтетический пример: у вас есть метод, извлекающий из коллекции элемент по некоему идентификатору целочисленного типа. И еще один метод, извлекающий элемент по целочисленному идентификатору, но другому.
Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID

Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови. Более того, придется называть наоборот, более многословно, чтобы пояснить, что же именно вот этот абстрактный оторванный от жизни кусок делает; тогда как в норме это была бы часть другого, более крупного метода и не нуждалась бы в пояснениях ни через комментарии, ни через название в силу наличия контекста.
Можно выбирать идентификатор через замыкание. Можно передавать параметр, который будет выбирать идентификатор. Можно придумать множество других способов.
UFO just landed and posted this here
Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови.

Не теряется, если разбивать по SRP.

Тоже думаю, что теряется, так как в пределе получается ассемблер.

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

Я не про оптимизацию, а про структуру в целом. Много мелких операций.

Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID

Что с ними не так?

Это довольно сложный вопрос. Если ваш программный продукт построен на фреймворках и там нет ни одной общей строки — тогда да.


Или вот, например, бывают такие методы.Возможно не очень понятно, что он делает сам по себе, но если вы находитесь в контексте работы приложения (а именно, это бот для работы с чатами), то в целом становится понятно.


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

Через полгода или просто если его другой человек откроет, уже будет сложно понять и контекст и смысл метода.
Агрумент про полгода, пожалуйста, уберите. Я его слышу уже больше 10 лет, но открываю свой код 10-летней давности… и эффекта не наступает. Да, конечно, мне приходится немного почитать свой собственный код, чтобы «вьехать» в то, что он делает — ну так и комментарии мне пришлось бы читать, какая разница?

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

UFO just landed and posted this here

Иногда люди не очень дружат с английским и использование становится странным и непонятным

UFO just landed and posted this here

Вы же понимаете, что код надо уже вчера, а английский минимум через неделю? А писать особо и некому, и чувак, по-случаю, вполне неплохо общается с ООП, алгоритмами и прочим, но вот английский дальше "a boy ate an apple" не зашел, но готов доблестно бороться с Google Translate.

UFO just landed and posted this here
Из вашего высказывания получается, что код писать могут только англоговорящие люди. И другим нет места в программировании? По-моему довольно спорное утверждение?
Ну смотрите: и так очевидно, что без английского сейчас сложно устроиться в нормальную контору, нельзя почитать доку на официальном сайте библиотеки и прочее. Но в конкретном примере человек даже не может читать и писать понятный код, то есть делает свою работу плохо.
И другим нет места в программировании?
Им скоро не будет места почти нигде. Пока ещё не так, но к этому всё движется. Мир интегрируется — хорошо это или плохо. Причём в программировании это происходит чуть быстрее ввиду совершенно естественных причин. Не умеешь читать/писать/разговаривать по-английски — считай, не умеешь читать/писать/разговаривать вообще. Конечно, людям с объективными ограничениями (болезнь, возраст и т.п.) стоит сделать скидку, я не спорю. Конечно, теоретически ситуация может поменяться (деглобализация, другой мировой язык или ещё что-то). Но пока выглядит как-то так.
UFO just landed and posted this here
Извините, если высказался слишком категорично.
UFO just landed and posted this here
Извиняюсь, подумал и понял, что был неправ. Фактически, мной (и, подозреваю, другими тоже — но не уверен) руководило во время написания этого комментария «всяк кулик своё болото хвалит». То есть у каждого человека есть какой-то свой набор умений (и неумений), и ему хочется верить, что именно его умения — самые важные (а его неумения — некритические). Это из серии «не служил в армии — не мужик», «каждый должен владеть компьютером» и «неумение водить машину в современном обществе равносильно инвалидности».

На самом деле никто не знает, какой именно набор умений наиболее важен (или оптимален при каком-то складе характера). Уж, по крайней мере, не уже-владеющему навыком об этом судить (а вот кого-то, кто бы говорил: «я не умею то-то и из-за этого пострадал там-то» — можно было было послушать). Допускаю, что существуют паттерны жизни, не завязанные на английский, просто я в них не разбираюсь.

+1 к комментарию SirEdvin, и стандартное пожелание: шлите патчи. Во многих open source проектах рук не хватает.

Скорее всего, Вы просто сначала думаете, что должен сделать данный программный объект, начерно проектируете его структуру и только потом приступаете к наполнению кодом. К сожалению, правило «Сначала пойми, что ты хочешь написать и только потом начинай кодировать» нынче не сильно в чести…
Как раз если сначала думать и проектировать структуру, то методы, классы и прочие логические единицы выходят самодокументирующимися.
Но на смеси английского языка и языка формальной логики. А так иногда хочется понять, что-же хотел сделать автор — ведь учителя английского у нас, скорее всего были, разные :-)
открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?
Не зачем, а «почему».

Мы с одним моим хорошим знакомым по этому поводу регулярно спорим. Для меня программа — это описание решения задачи, но она, в первую очередь, написана на C++ (Java, C#, PHP — нужное подчеркнуть). А комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.

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

Для моего же знакомого комментарии — это основное, текст программы — это, так, «что-то такое для компилятора», а описание программы должно быть на «человеческом» языке. И он начинает «кипятком писать», когда в ответ на вопрос «а что сюда, собственно, передавать» он получает ответ «ну из кода же это очевидно!».

Почему среди OpenSource преобладают люди первого вида, а «за деньги» — в основном работают люди второго вида я не знаю…
комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.
Иногда бывает, что красивый, стройный, понятный алгоритм… тупит. Выполняется не приемлемое количество времени. И вот тогда начинается черная магия с рекурсией, вложенными циклами с next/prev, временными переменными, etc. Как вы считаете — надо оставить понятно и медленно, или странно и быстро — но с несколькими строчками комментариев, объясняющих «магию» (и предупреждение «ничего не трогай!» :) )
Зависит от того, критичен ли для вас этот код и можете ли вы себе оставить медленную версию.

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

Я не против длинных комментариев, более того — мой рекорд это описание строчек примерно в 50 к фукции из одной строчки (с отсылками на места в разных версиях C и C++ стандартов, обьясняющих почему этот код не просто «случайно здесь работает», а будет работать на всех реализациях, совместимых со стандартом).

Но сам принцип — «комментарий == сноска с пояснением в книге» для меня по прежнему является основным…
Если код тупит, то скорее всего, это не из-за читаемости кода, а из-за того, что по другому не захотели делать. И от «рефакторинга» добавлением комментариев в духе "// тут рекурсия и вложенный цикл" лучше не станет.

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

UFO just landed and posted this here
Они просто приходят к 9 и уходят в 6.

Вы так говорите, будто это что-то плохое.
UFO just landed and posted this here
Я все чаще сталкиваюсь с мнением, что быть доступным для работы 24/7 — это признак успеха, причем, что самое милое, у наёмных работников. В то время, когда в подавляющем большинстве компаний творится всякий scrum и все подчинино расписанию, то подобное кивание на часы выглядит странно. Не смог продавить свой чудо-рефакторинг на планировании? Так это ты ССЗБ стахановец, а не те, кто отрабатывает приоритетные таски в заложенные часы.
UFO just landed and posted this here
не фиксить их проактивно, ждать пожара… В ответ надо выдать эстимейт достаточный для хорошего рефакторинга

Хитрая тактика (= Хотя, если вы не зарылись в «пожарах», вокруг все не так уж плохо.

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

Мне кажется, что проблема здесь начинается с начальства, которое не волнует качество, а нужно видеть озабоченные лица за компьютерами с 9 до 18. В гос.конторах и других крупных бюрократических организациях такое практикуется.
UFO just landed and posted this here
Пустые офисы? Врятли… если работа настолько проста… просто подкинет ещё в топку, и так до того уровня на котором работники будут едва справляться и постоянно загружены. Но тут возникнет другая проблема — люди в таких условиях быстро выгорят и перестанут работать вообще.
У ЧакНориса есть такое выражение — «комментарии плохо пахнут».
Т.е. надо писать код так, чтоб комментарии вообще не требовались… Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста. Плюс такого подхода — названия переменных и функций должны быть гипер-продуманными и понятными. Ну и кода без комметариев на мониторе больше видно.

Видимо, в open source кадый мнит себя ЧакНорисом :)
Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста.
Если комментарий противоречит коду, то есть проблема, да.
Иногда проблема в том, что код не соответствует замыслу программиста с самого начала.

А если комментария нет, то проблема не видна: код соответствует тому, что он делает, а не тому, что он должен делать.
Иногда проблема в том, что код не соответствует замыслу программиста с самого начала
Есть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)
Есть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)
В десятку. Код всегда соотвествует программе. Он может не соответствовать замыслу, он может не соответствовать каким-то великим идеям, но он всегда верно и точно описывает то, что делает программа на самом деле.

А чтобы работать с программой мне, в общем-то, это и нужно. Какое мне, собственно, дело, до тех идей, которые роились в голове у человека, когда он это писал? Да никакого! Мне важно как оно здесь и сейчас работает!
Так речь о том, что код может соответствовать замыслу в одной реализации компилятора, но не соответсвовать в другой
Ну такой код хоть где-то хоть чему-то но соотвествует. Комментарий вообще может не иметь отношения к тому, что программа делает.
Да никакого! Мне важно как оно здесь и сейчас работает!

А почему вы по умолчанию считаете, что оно работает? :-)

Оно всегда работает. Даже если автор породил код путём соединения рандомных кусков со stackoverflow руководствуясь принципом Пусть будет, как будет — ведь как-нибудь да будет! Никогда так не было, чтобы никак не было.

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

Потому что я видел такие комментарии.

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

Могу поверить что подобное иногда происходит, но в моей практике гораздо чаще бывает так, что код работает (пусть и не совсем так, как его автор предполагал), а комментарий — неверен.

Возможно где-нибудь в аэрокосмической отрасли написание программы дважды (один раз в комментариях, второй — «перевод» на язык программирования) имеет смысл, но в большинстве случае IMNSHO это излишне.

Зачем вы все время говорите про дублирование? Комментарии не должны дублировать реализацию.


// прямое обращение, потому что работает быстрее
x->a = b;
// хак, чтобы сработал сеттер, так как ...
x->a = b;

В зависимости от языка и описания x цели одной строчки могут быть совершенно разные.

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

Ну обманывает, прям проблема.
Да, таки проблема.

Заметил, что не соответствует коду — возьми и поправь.
Этого невозможно заметить, если вы не читаете код, а читаете только комментарии. А если вы читаете код и понимаете его настолько, что можете исправить и комментарий — то зачем вам там комментарий вообще? Он только к лишней трате времени приведёт.

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

А кто сказал, что надо читать только комментарии?


то зачем вам там комментарий вообще?

Затем, что он описывает то, чего в коде нет. "Здесь сделали так, потому что быстрее работает".


обязательно описывать все функции

Это документация, а не комментарии, дублирующие код. Внутренняя реализация может измениться, но если внешнее поведение не изменилось, то и документация не изменится. И наоборот, документация к функции может дополняться без изменения кода. Документация — это не просто комментарии, для нее иногда даже особый формат комментирования делают.

А кто сказал, что надо читать только комментарии?
Никто не сказал. Но обычно люди, жалующихся на острый недостаток комментариев, как выясняется, код читать не хотят вообще. Или, по крайней мере, читают его когда что-то непонятно из комментария. Что, как мне кажется, извращает саму идею довольно сильно: комментарий должен прояснять код, а не код — являться разъяснением спорных моментов в комментарии!

Это документация, а не комментарии, дублирующие код.
В 90% случаев функции делают что-то относительно несложное (что именно — описано в названии, параметры описывают что на входе и что на выходе) и это именно что дублирование даже не кода, а заголовка функции. В случае, когда функция делает какое-то нетривиальное действие комментарии, разумеется, уместны — но таких функций, обычно, немного.
Люди обычно жалуются на недостаток комментариев в непонятных местах, а не вообще по коду.
Почитайте топикстартера: Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000.

Жалоба была именно на то, что нет комментариев «вообще», а не на то, что какое-то сложное место не описано.
Бывает, пытаешься понять

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

Ориентироваться же по комментариям в коде — это всё равно что пытаться понять что есть в книге не по оглавлению, а по сноскам.
Очень хороший пример. Отдельное описание структуры из которого понятно — что делает весь остальной код. Вместо кучи странных замечаний из которых получается ребуйс…
Самая большая моя боль была при первом столкновении с библиотекой, завязанной на glib. Куча функций вообще без единого коммента. Ладно, внутри функций, бог с ним. Но хотя бы о самой функции — что за аргументы, что она возвращает? Нет, зачем, и так же все понятно.

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

Ох и намаялся я, пока все утечки памяти выискивал.

Вот потому-то и надо умные указатели использовать...

В библиотеке на C (не C++)? Как вы это себе представляете?

Отказаться уже от Си. Это была шутка.

Так это и были умные указатели. Только на С. Поэтому ref/unref нужно вызывать руками.
Т.к. многие функции возвращали указатели, постоянно приходилось угадывать, нужно ли потом память по этим указателям освобождать самостоятельно? Или нужно вызывать g_object_unref? Или можно вообще ничего не делать?
А документацию прочитать — не судьба?
А как документация для glib поможет мне понять вот эту функцию из библиотеки, которая от glib зависит?
const char *
arv_device_get_string_feature_value (ArvDevice *device, const char *feature)


Она возвращает указатель на строку. Эту строку нужно освобождать? Или не нужно? Комментариев к функции или внутри функции нет вообще. Строка прямо в ней не формируется, она берется из другой функции.

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

Судя по git blame комментарии-шапки добавлялись для какого-то генератора биндингов, видимо, некомментированные функции этому генератору были не нужны.

Хех… типичный пример API функции у BLAS:
csymm (SIDE, UPLO, M, N, ALPHA, A, LDA, B, LDB, BETA, C, LDC)
Нужно больше боли ;-)

Модные best practices настаивают, что комменты это плохо и все функции\переменные должны быть самодокументируемыми. Поэтому современные каргокультисты от программирования считают коментарии моветоном и максимально их избегают.

Я Вам более того скажу — молодые программисты на C++ написанием такого кода просто бравируют — "я могу понять как этого будет работать/вычисляться, а моему тимлиду надо пойти освежить свои знания или поломать голову чтобы разобраться".

Для себя вывел правило. Если видишь на собеседовании такой код — однозначно попал в молодой и дружный коллектив.
А я pk для себя расшифровываю как primaryKey (при работе с БД)
Дело в том что когда программист плотно работает с конкретными терминами и сокращениями для него они становятся очевидными, а длинные имена часто используемых переменных — раздражающе длинными.

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


Ещё бесят сокращения вроде privKey (в последнее время работал с кодом для цифровых подписей). Почему не написать privateKey, всё равно ведь IDE дописывает слова сама? Короче, вижу много сомнительных решений и делаю выводы для себя чтобы писать более приятный для чтения код:)

Когда требуется делать сокращения (например, полное имя идентификатора состоит из слов так 5-6), пишу пояснение в том месте, где происходит объявление. Чтобы человек, читающий код, понимал мою «логику сокращений».
В чем проблема делать псевдонимы с ссылками на полную версию. Тогда для опытных участников — код всегда будет лаконичным, а для стажеров — понятным.
И поддерживать потом обе версии?
Самое хреновое, когда писатели такого кода начинают учить (или заставлять) других как надо писать.
Комментарии конечно вещь полезная, но тут всё зависит от ситуации.
В качестве примера он привел одну успешную книгу популярного автора, который свято верил в то, что чем короче код, тем быстрее он работает.

Кто-нить знает что это за книга? Стало интересно почитать.

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

На уровне компилятора любая двусмысленность должна порождать либо Warning, либо ошибку компиляции по причине undefined behaviour. Странно, что разработчики компилятора об этом не заботятся, а потом приходится юзать всякие PVS Studio и пр.
Интересно бы услышать мнение Andrey2008

Я думаю, это ближе к особенностям языка. Зачем то же такие выражения как ++i и i++ были нужны…

Обычая перезакладка. Недаром в Go оставили только i++ и не как операцию, а как инструкцию.

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

Это надо смотреть на ассемблеры некоторых ВМ тех лет.

UB — это не ошибка компилятора, а невыполнение программистом контракта.


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


В таком случае компилятор делает сложение с помощью одной инструкции, а не 5, но налагает определенный контракт на разработчика. Если программист не выполнил соответствующий контракт — то поведение не определено (собственно, UB), компилятор имеет право сожрать ваши ботинки.

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

Вот и неплохо было бы Warning получать, чтобы не держать в голове все детали "контракта".

Будет слишком много ложноположительных срабатываний и warning просто будет после этого отключен.


В том же rust пошли немого другим путём и в debug-сборке, например, overflow при сложении знаковых целых приведёт к панике (исключению), но debug-сборка там обычно работает в разы медленнее. В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си. А кому нужна именно сумма с переполнением напишет overflowing_add явно.

Вопрос из интереса: а можно ли rust-компилятору сказать, что integer overflow сделан умышленно и ошибки здесь нет?

https://doc.rust-lang.org/std/primitive.i32.html, см. checked_add, saturating_add, wrapping_add и overflowing_add. Выбираете нужный явно и никто потом не гадает что имелось ввиду и есть ли какие-нибудь особые куски контракта не отраженные в коде.

Решение — тривиально до невозможности. В gcc/clang тоже добавили — вот только непонятно почему на это потребовалось чуть не полвека мучений…

Оно формально intrinsics?


А мучения, видимо, чтоб жизнь мёдом не казалась. И в стандарт доедут году к двадцатому..

Оно формально intrinsics?
Угу. Код достаточно оптимальный порождается, семантика тоже определена.

И в стандарт доедут году к двадцатому..
Экий вы оптимист, батенька…
Экий вы оптимист, батенька…

Я просто работая с поделиями по развитию застрявшими в конце девяностых немного забыл, что уже 17 год)) Так что исправляюсь и скажу, что ждем и надеемся на C37 ,)

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

Писать игры на ASM такое себе удовольствие. Особенно, когда используешь какой-нибудь UnrealEngine или еще какой-нибудь движок.

Вот именно поэтому я и не пишу игры.
А вообще это был сарказм.
В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си

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

Это неотличимо от implementation defined, т. к. существует только один компилятор, да.

Ну в "reference" языка данное требование прописано, но я затрудняюсь судить какой статус будет у реализации (частично) игнорирующей такие требования.

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

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

Вы готовы пожертвовать 80-90% производительности для десктопа? Получить десятикратное увеличение стоимости услуг и программ? Если да, то есть простые компиляторы с простой кодогенерацией, но в конкурентной гонке general purpose софта побеждают отнюдь не надёжные и медленные программы, которые имеют 5-10% нужного функционала. Пока нет специальных требований на надёжность при допустимой низкой производительности (как в, например, hard realtime), никто этого делать не будет.


При этом есть большая ниша прикладного софта, где нет проблем с производительностью. И пишут её на java/c#/python, где проблемы UB не стоит, т. к. managed окружение с примерно одной виртуальной машиной (на каждую из платформ) за счёт TCK и подобных решений. В случае питона это не совсем так, но близко.

Откуда такие цифры? Хотите сказать, что 80-90% производительности для десктопа достигается за счет кода с UB?) Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.

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

Без, но с учётом же. Таким образом у компилятора есть возможность проводить некоторые свои оптимизации.
Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.
Вы это серьёзно?

Пример кода, который потенциально может вызвать UB: a = b + c;. Или ещё так: free(p);. Или, на худой конец: i++.

Много вы программ видели, которые ничего этого не содержат? Или где такого кода очень мало? Я видел — это обычно 100500 обёрток, которые и без всякого UB тормозят так, что им никакой компилятор не поможет…
a = b + c
Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению. А не запускает форматирование жесткого диска. Это я и имел в виду под «генерировать максимально близкий машинный код».
Вы неправы. Он вполне может и не сгенерировать, если посчитает, что так быстрее, а результат будет тот же самый (кроме результатов при UB)
Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению.
Простейший компилятор — да, может быть. Да и отптимизирующий тоже, если это выражение присутствует в отдельной функциии и ничего другого там нет — но кому такая функция нужна?

А вот если «чего другого» там есть, то оптимизирующий можен много чего со всем этим сделать. Может засунуть константу прямо в оператор обращения к памяти, например. Или перенести её куда-то. Да много чего можно сделать если знать, что переполнения не будет. А его не будет, так как программист обещал!
И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c. Даже если a появляется только во внутренних регистрах процессора при вычислении сложной адресации.
И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c.
Не будет в a суммы b и c. Потому что оно туды не влезет. В результате у вас индекс, который, вообще говоря, планировался быть short'ом после оптимизаций окажется равным 70000 — и вы будете материть компилятор на чём свет стоит.

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

А дальше, если за этим не следить, получается «снежный ком» — тут у нас оказалось 70'000 в short'е, там — мы полезли не в тот обьект, вынули не то, засунули не туда… и вот уже ваша программа самоуничтожается. До форматирования винчестера дело [пока?] не дошло — но всё ещё впереди!

Если после оптимизаций получилась сумма 70000 при входе 2 и 2, значит это неправильные оптимизации. Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000. Потому что это понятное и логичное поведение. Тем более, что компилятор его предупреждал. Поэтому непонятно, откуда у вас получился снежный ком.


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

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

А где грань? Почему в случае a = b + c предполагается доверять программисту что тот ничего не забыл — а в случае, условно, a = b->c; надо программиста предупреждать?

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

Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000.
В том-то и дело, что может получить в результате оптимизаций.

Тем более, что компилятор его предупреждал.
Он его на каждую операцию сложения предупреждал? И программист этого не отключил? Это какой-то неправильный программист и он явно занимается неправильным делом. Ему мандалы из песка делать нужно.

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

Написано, значит должно появляться, или не компилироваться.
Есть маленькая проблеммка: узнать — вызывает программа UB или нет, в общем случае, невозможно. Проблема остановки, мать её. Потому компилятор исходит из того, что программист — сам себе не враг, UB не допускает и из этого исходит.

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

Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?


Он его на каждую операцию сложения предупреждал?

Я не особо специалист в C++, наверно чего-то не понимаю. То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?


вызывает программа UB или нет, в общем случае, невозможно

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

Оптимизирующий компилятор может увидеть после операции сложения операцию вычитания, деления или ещё какую, и поставить её раньше, если решит, что так нужно для оптимизации. А может и не поставить.
Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?
На многих процессорах нужно предпринимать специальные усилия, чтобы «обрезать» число (x86 — редкое исключение, а не правило) и компилятор, зная о том, что переполнений не бывает вполне может производить вычисления с большей точностью. В x86 так тоже может быть — но в довольно специфических условиях.

То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?
Любая операция сложения потенциально может приводить к UB, если результат «не влезет» в соответствующий тип. Компилятор, в общем, не так часто может понять — будет результат «влазить» или нет. Так что «с консервативным подходом» ему придётся выдавать предупреждения на каждую операцию сложения, про которую он ничего не сможет доказать. То есть про большинство из них.
Вы так говорите, словно в компиляторах есть строчка «if (isUndefinedBehavior) system(»format C:\");".
в стандарте языка есть вещи, которые компилятор обязан отслеживать. И они это делают. Соответственно, все оптимизации производятся с учетом этих требований. А неопределенное поведение потому и неопределенное, потому что даже разработчик компилятора не скажет, во что оно выльется.

Так разработчик компилятора и не должен ничего говорить. Неопределенное значит неопределенное, пусть процессор и ОС разбираются. Не надо его выбрасывать или заменять на другое, как в примере с циклом. Даже там в комментах шутят про "rm -Rf /". Просто по-моему это не то, что должен позволять стандарт языка.

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

Причем здесь "смиритесь"? Это обсуждение причин и возможностей, а не pull request в стандарт. Поговорить на эту тему теперь тоже нельзя?

Поговорить-то можно, но проявлять воинствующее невежество не стоит.

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

В неприятии вами того факта, что эффективность оптимизаций, вызывающих UB и не вызывающих может отличаться на порядок.
Строго говоря ему ещё не удалось привести пример оптимизации, которая не приводила бы к неработоспособности чьй-нибудь программы. Я, впрочем, пример такой оптимизации привести могу. Если у вас в программе написано два раза "mov eax, ebx" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov. Я не знаю — как это «заметить». Хотя, может быть, и тут можно, просто у меня недостаточно богатое воображение.

Но много вы на таких оптимизациях получите? А просто заменить «лишний» mov, как было нашим горе-воякой предложено — нельзя.

P.S. Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью. Иначе — никак. Даже в Go и Java — полно UB, потому что манипулировать миром, на который кто-то может смотреть «сбоку» и что-то там изменить так, чтобы этого не стало заметно — практически невозможно.
Если у вас в программе написано два раза "mov eax, ebx" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov.

При чуть более сложном варианте вида mov eax, [esi] два раза уже, по сути, заменить два movа на один нельзя даже в однопоточном коде при запрещенных начисто прерываниях. Вдруг по адресу из esi лежит mmio-регион.

А просто заменить «лишний» mov, как было нашим горе-воякой предложено — нельзя.

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


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

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


Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью.

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

Дайте ссылку на коммент, где вы привели пример с разворачиванием цикла? Или вы имеете ввиду «повторить цикл 8 раз, значит надо делать его 8 раз»? Как раз здесь вполне можно оптимизировать вплоть до того, что ничего не повторять ни одного раза, если результат (не считая случаев с UB) от этого не изменится.

Да. "Можно 8 раз скопировать действия без цикла и выбросить сравнение i".


Как раз здесь вполне можно оптимизировать вплоть до того, что ничего не повторять

В том примере ничего не повторять не получится, так как заполняются значения по указателю. А в целом да, если в цикле одно действие x = 2;, то цикл можно убрать, так как результат не изменится. Здесь нет никакого противоречия моим словам. "значит надо делать его 8 раз" было сказано в контексте того примера, так как там поведение цикла распространяется за пределы цикла и функции.

Чем это принципиально отличается от оптимизации в вашем примере?
Ничем — и в этом-то всё и дело.

Покажите пожалуйста, где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное?
Дык эта:
Я считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть.
Вот прямо-таки туточки.

Вы либо трусы наденьте, либо крестик снимите. Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB («считает такты или что-то подобное»), либо нет.

Потому что пока ваши хотелки выглядят так: копилятор имеет право делать для оптимизации что угодно — но не должен ломать моих программ… чужие — можно.

Так не бывает, уж извините. Нужен полный и точный перечень того, чего «хорошая», «правильная» программа делать не должна (в частности, по вашему — не должна считать такты). И вот этот список — и есть список UB, на которые полагается ваш компилятор.

Компилятор, который оставляет все UB как есть — не может оптимизировать, фактически, ничего и никак…
Ничем — и в этом-то всё и дело.

Почему вы тогда сказали, что "просто заменить нельзя", если компиляторы проводят такие оптимизации? Раз ничем не отличается, значит можно.


Вот прямо-таки туточки.

Не вижу связи. Не могли бы вы прямо по пунктам написать логические выводы, приводящие к противоречию? Мне правда интересно, возможно я что-то не так понимаю.
Оптимизации бывают не только из-за UB. И не каждую ситуацию из тех, которые в стандарте C++ называются UB, нельзя оптимизировать. Я ниже привел примеры под спойлером, что я имею в виду под "не должен полагаться на UB".


чужие — можно
в частности, по вашему — не должна считать такты

Нет, это не по-моему, это вы сами додумали. Я наоборот говорю, что не должно быть требований, что программа что-то должна. Считает — ее дело, пусть программист учитывает, что может быть оптимизация.


Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB, либо нет.

Оптимизатор должен обеспечивать то же поведение. Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же. А не a = 42;


не может оптимизировать, фактически, ничего и никак…

Я же привел примеры. Развернуть цикл можно? Можно.

Оптимизации бывают не только из-за UB.
Оптимизатор полагается на то, что некоторые действия являются UB. И, соотвественно, в программе не встречаются.

Почему вы тогда сказали, что «просто заменить нельзя», если компиляторы проводят такие оптимизации?
Соблюдая ваши «правила игры» (компилятор наоборот не должен полагаться на UB и должен оставлять его как есть) — нельзя. По правилам игры, в которую «играют» разработчики компиляторов — конечно можно!

Я наоборот говорю, что не должно быть требований, что программа что-то должна. Считает — ее дело, пусть программист учитывает, что может быть оптимизация.
Круто. У вас буквально две соседние фразы противоречат друг другу! Я впервые вижу такое проявления двоемыслия в споре.

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

Оптимизатор должен обеспечивать то же поведение. Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же. А не a = 42;
Пример вашей «хорошей» оптимизации это требование нарушает. До оптимизации выход из цикла был возможен, после — нет.

Я же привел примеры. Развернуть цикл можно? Можно.
В общем случае — нельзя.

Рассмотрим практический пример:
uint64_t Read64A(const uint8_t* src) {
  uint64_t result = src[0];
  for (int i=1;i<8;i++)
    result |= (uint64_t)src[i] << (i * 8);
  return result;
}

Развёрнутый (и затем свёрнутый) цикл:
Read64A(unsigned char const*):
        mov     rax, qword ptr [rdi]
        ret

Казалось бы — великолепная оптимизация, слава компилятору!

Одна беда — если теперь вы в эту память будете из другого потока писать либо 0x0000000000000000, либо 0xffffffffffffffff, то прочитать оттуда 0x00000000ffffffff вы не сможете ни за что и никогда. А оригинальный код это сделать мог. И это вполне могло помогать кому-то программу.

P.S. Только не надо про то, что «стало только лучше» и «скорее всего программист этого и хотел». Эти критерии — неформализуемы и их, в общем, невозможно использовать для того, чтобы решать — является ли что-то хорошей оптимизацией или нет.
Оптимизации бывают не только из-за UB

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

А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.
Развернуть цикл можно? Можно.

Какая разница, сегодня или месяц назад, главное собаку-то я покормил!
P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
О невозможности UB. Невозможности, которую должен обеспечить программист.

Любая оптимизация опирается на тот факт, что программу у нас «хорошая», чего-то «плохого» не делает. Вот если это «плохое» в принципе «железо» позволяет сделать — это и есть UB.

Понятно, что тут всегда встаёт вопрос компромиса: что именно мы можем заставить разработчика не делать, а чего не можем. То есть вопрос отнесения чего-то к UB — это вопрос обсуждаемый. Что-то, что стандарт называет UB, конкретный компилятор может и не называть UB, а, наоборот, допускать что в программе такое происходит. А может иметь свои UB (обычно считается, что это «дефект, который когда-нибудь пофиксят», но это когда-нибудь может растянуться на долгие годы). Но если уже что-то отнесено к UB — то, разумеется его в программе быть не должно и компилятор вправе на это полагаться.

Выдача же при этом полезных предупреждений — отдельная и весьма сложная задача.
А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.

Из исходного кода. Если в исходном коде присваивание было только при условии, то после оптимизации не должно присваиваться всегда. Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет. PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.


А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.

Вы упорно не хотите понять, что я пытаюсь объяснить. Случаи, которые можно назвать UB, бывают разные. Если коротко, то что делает PVS-Studio, должен делать компилятор.

Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет.
Но если человек её не проинициализировал, то это значит что ему всё равно что оттуда вернётся — так почему бы и не 42? Кода меньше, а в правильно написанной программе — это «стрелять» не должно.

PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.
PVS-Studio — это отдельный продукт, специально «заточенный» под то, чтобы отлавливать ошибки в программах. А компилятор — это компилятор. Компилятор решает одну задачу: сделать так, чтобы программа не вызывающая при своём исполнении UB работала быстро и требовала мало памяти. Всё остальное — не к компилятору. Не надо превращать компилятор в Die Eierlegende Wollmilchsau, пожалуйста, это приведёт только к тому, что все задачи будут решаться одинаково плохо.
Тот же «исходный код» может быть библиотекой, которая используется в 100500 разных проектах. И та или иная часть её может никогда не понадобиться. И без соответствующих оптимизаций придётся либо при каждом вызове проверять кучу условий, которые всегда не выполняются, крутить ненужные циклы и пожертвовать 90% скорости на ненужный мусор. Либо нагородить 100500 вызовов, делающих одно и то же, но чуть по-разному.
С этим никто и не спорит. Можно просто взять и выбросить половину программы, работать будет быстрее, только не так как ожидалось. Я говорил, что компилятор не должен делать это молча. А спорю я потому что мне говорят, что это правильно и по-другому никак. И при этом нет особых доказательств кроме «в стандарте так, смиритесь».

Кстати, вы можете привести пример таких оптимизаций? Нашел в гугле пару ссылок, попробую объяснить, что я имею в виду.
Скрытый текст
http://en.cppreference.com/w/cpp/language/ub#UB_and_optimization
Signed overflow
int foo(int x) {
    return x+1 > x; // either true or UB due to signed overflow
}
may be compiled as (demo)
foo(int):
        movl    $1, %eax
        ret


Это явно ошибка в логике — условие всегда истинно или происходит переполнение.

Access out of bounds
int table[4] = {};
bool exists_in_table(int v)
{
    // return true in one of the first 4 iterations or UB due to out-of-bounds access
    for (int i = 0; i <= 4; i++) {
        if (table[i] == v) return true;
    }
    return false;
}
May be compiled as (demo)
exists_in_table(int):
        movl    $1, %eax
        ret


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

std::size_t f(int x)
{
    std::size_t a;
    if(x) // either x nonzero or UB
        a = 42;
    return a;
}
May be compiled as (demo)
f(int):
        mov     eax, 42
        ret


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

https://www.cl.cam.ac.uk/teaching/1415/CandC++/lecture10.pdf

By knowing that values “cannot” overflow, the compiler can enable useful optimisations:
for (i = 0; i <= N; ++i) { ... }
If signed arithmetic is undefined, then the compiler can assume the loop runs exactly N+1 times.

Вот и пусть 'assume', потому что это то, что написано. Если там будет i <= INT_MAX, это будет известно на этапе компиляции, и можно показать предупреждение.

Это между прочим те вещи, которые ищет PVS-Studio. Такая оптимизация маскирует ошибки, хотя задача компилятора эти ошибки находить. Программа работает быстро, но не так как надо.

У меня сложилось впечатление, что UB называют очень логически разные ситуации, часть из которых подходит для оптимизации, часть нет. Если вы считаете, что я не прав, значит так тому и быть. Я просто высказал мнение и постарался объяснить, почему я так думаю.
UB называют любые ситуации, когда результат выполнения инструкции не регламентирован в стандарте.

Это я понял. Я имел в виду, что они все-таки различаются, и работать со всеми одинаково нельзя, потому что это приводит к проблемам.

работать со всеми одинаково нельзя

Что значит «одинаково работать»?

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

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

Решать будет компилятор. Только надо разделить понятия "UB из-за неинициализированной переменной" и "UB так как возможно кто-то 8 байт пишет из другого потока". В первом случае это написано в коде, во втором нет.

Только надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока».
Они уже разделены. Я боюсь вы смешиваете два понятия: поведение, определяемое реализацией (что-то, что разные компиляторы могут делать по разному) и неопределённое поведение (то, чего в программе случаться не должно и то, чего программист не должен делать никогда).

В первом случае — компилятор обязан обеспечить некоторое разумное поведение (скажем если вы засунете 2147483648 в 32-битный int, то получите либо -2147483648, если у вас используется дополнительный код, либо -0, если у вас используется прямой код, но ничего «странного» при этом произойти не может), во втором — компилятор может делать всё, что угодно. Совсем что угодно.

Зачем вам потребовалось ещё как-то этот волос расщеплять и кому от этого станет легче — мне неведомо.

В первом случае это написано в коде, во втором нет.
Что значит «написано» и «не написано»? 8 байт кто пишет? И как? Пушкин? С того света, что ли? Конечно же в программе есть где-то код, где и эти 8 файт атомарно пишутся…
Такая оптимизация маскирует ошибки, хотя задача компилятора эти ошибки находить.
Задача компилятора — компилировать. Для нахождения ошибок — есть другие инструменты. То, что компилятор, по совместительству, является ещё и статическим анализитором и линтером — это хорошо, но это не является его основной работой!

int foo(int x) {
    return x+1 > x; // either true or UB due to signed overflow
}
may be compiled as (demo)
foo(int):
        movl    $1, %eax
        ret

Это явно ошибка в логике — условие всегда истинно или происходит переполнение.
Так имеет право компилятор сделать такую оптимизацию или нет?

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

Все же UB, которые не «доопределены» подобным образом компилятор имеет право «использовать» — в смысле «трактовать в свою пользу».

Я просто высказал мнение и постарался объяснить, почему я так думаю.
Вы бы вначале сформулировали своё мнение, а потом его высказывали бы, а? А то сейчас у вас получается что компилятор должен и предполагать, что программа «глупостей не делает» (где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное?), и предполагать, что они «глупости там таки есть» ( считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть). Одновременно.

Давайте возьмем гугл и посмотрим на производительность tcc от Bellard'а:
https://groups.google.com/forum/#!topic/comp.lang.c/9l55qxm-S68, http://lists.nongnu.org/archive/html/tinycc-devel/2013-02/msg00039.html. Потери производительности от 2х раз до порядка (относительно gcc).


Замечу, что он хоть и относительно простой, но оптимизации там есть. И UB из языка C там никуда не исчезает, более топорная кодогенерация и меньше шансов на странные побочки от UB. Но при этом компилятор всё равно полагается на то, что программист не допускает нарушения соответствующих контрактов.


Без этого банальный инкремент int'а будет занимать не одну инструкцию, а 3-4 с условным переходом, что будет забивать мозг предиктору переходов (который аппаратный и "память" у него короткая), дёргать дополнительно ALU для сравнения или читать дополнительно флаги. Всё это спокойно может давать overhead в десятки тактов.


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


Но при выполнении контракта (недопущении UB программистом) вы получите работу инкремента за одну инструкцию.

Без этого банальный инкремент int'а будет занимать не одну инструкцию, а 3-4 с условным переходом

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


более топорная кодогенерация

Это не то, что я имел в виду.

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

После какого количества warning'ов вы их отключите? После первых нескольких тысяч? Или раньше?


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

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

Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении. Или вообще не компилируем.
Это примерно то, что делает tcc. Потери производительности, как вам уже сказали — от 2х раз до 10. Хотя он не вполне неоптимизирующий.

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

В примере, который я уже приводил компилятор всего-навсего заметил, что у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла (а они, как бы, время занимают, да) — и использовать для выхода из цикла эту переменную. Законно так сделать? В отсутствие переполнений — да, разумеется. Простая линейная замена. Весьма и весьма полезная в разных матричных алгоритмах.

А дальше — другой код, про UB ничего не знающий проверил когда в «усовершенствованной» программе происходит выход из цикла. И выяснилось что для этого число должно стать больше максимального числа, который может поместиться в int. То есть у нас там — проверка, которая всегда ложна. То есть выход из цикла не случится никогда. Законная такая замена? Разумеется — опять-таки в случае отсуствия UB.

А в результате — цикл вместо заполнения таблички уничтожает всю вашу программу на корню.

В том-то и дело, что вы либо «додумываете за программиста и выкидываете просто так» куски кода — тибо у вас «более топорная кодогенерация». А как иначе? Как, по вашему, компилятор должен что-то оптимизировать, если он ничего никуда не может подвинуть и ничего ни откуда не может удалить?
у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла

Неправильно. Во-первых, переполнения существуют, и компилятор должен об этом знать. Во-вторых, написано, повторить цикл 8 раз, значит надо делать его 8 раз. Написано, выйти из цикла, когда переменная i станет 7, значит надо сравнивать i и 7. Можно 8 раз скопировать действия без цикла и выбросить сравнение i. Но не заменять одну проверку другой. Это именно то, о чем я говорю — компилятор додумал за программиста. А дальше да, снежный ком.


Как, по вашему, компилятор должен что-то оптимизировать

Пример:
mov eax, ebx
mov ebx, eax


Заменяется одной инструкцией:
mov eax, ebx


Результат со всеми изменениями в состоянии регистров тот же самый.


Оптимизация? Оптимизация.

Далеко на таких оптимизациях не уедешь. К тому же даже такая оптимизация имеет допущения, что никто не должен читать регистры между выполнением инструкций, что вы не считаете такты процессора. Пусть обратное кажется невероятным.
К тому же даже такая оптимизация имеет допущения, что никто не должен читать регистры между выполнением инструкций, что вы не считаете такты процессора.
Ну считать такты в современных процессорах никто не будет, а менять регистры — таки да. Я с таким лично сталкивался в JIT-компиляторе. Но это редкость. Но если учесть ещё, что C, в общем-то, обычно не все переменные в регистрах (более того, в C++20 их вообще нельзя будет на регистры класть), то окажется, что вам вообще ничего трогать нигде нельзя будет — ибо sigaction/kill вдруг работать перестанут (ну или многопоточные программы вспомните, если у вас нет в OS работающих sigaction/kill).
Смотря в каких процессорах опять же, наверняка ещё есть куча микроконтроллеров, в которых гигагерцы тактовой частоты и таймеры — непозволительная роскошь. Хотя с другой стороны, если считать такты, писать надо сразу на ассемблере.

Это применимо к любой оптимизации.

Правильно — потому требование «не додумываем за программиста и не выкидываем просто так» запрещает все и всяческие оптимизации.

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

Правильный код на C/C++ не считает такты процессора, не меняет регистров из обработчика сигналов, не обращается к переменным, которые были освобождены c помощью free и к переменным вообще из разных потоков без синхронизации (за исключением строго описанных случаев — см. volatile)… в общем код — не вызывает UB никогда и нигде.

Вот тогда его можно как-то оптимизировать. Иначе — никак.

Я же написал, как можно сделать оптимизацию в приведенном вами примере без изменения ожидаемого поведения. "Ожидаемое" — это то, что написано в исходном коде. Значит иначе тоже можно.

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

«Ожидаемое» — это то, что написано в исходном коде.
Нельзя. Для этого нужно «знать» что происходит во всей программе.

Рассмотрим ваш пример и немного его расширим.

Неоптимизированный (работающий) код:
  mov 1, eax
back:
  mov eax, ebx
  mov ebx, eax
  test eax, eax
  jnz back

Оптимизированный (неработающий) код:
  mov 1, eax
back:
  mov eax, ebx
  test eax, eax
  jnz back

Как, почему, зачем, за что? А очень просто: я беру sigaction/pthread_kill и по сигналу из другого потока обнуляю ebx (если вы любитель Windows, то для вас есть GetThreadContext/SetThreadContext).

И всё. Ваша «безопасная и надёжная» оптимизация превратила работающую программу — в неработающую. Неоптимизированный код работу завершает, опимизированный — нет.

И? Что вы после этого будете говорить? Что в программе «такого ужаса» быть не должно? Или что ваши оптимизации плохие?

Если первое — то вы только что сами ввели понятие UB. Если второе — то всё ещё остаётся вопрос какие оптимизации являются хорошими.
Ожидаемого кем? Ожидаемого когда?

Ожидаемого от программы кем-то, кому нужен ее результат. Ожидаемого в результате компиляции кода. Ожидаемого во время его работы. Что за философские вопросы?


Как, почему, зачем, за что? А очень просто: я беру sigaction/pthread_kill и по сигналу из другого потока обнуляю ebx
И? Что вы после этого будете говорить?

А причем здесь другой поток? Компилятор про него ничего не знает, у него есть только исходный код. Я уже несколько раз это говорил. Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.


Ваша «безопасная и надёжная» оптимизация превратила работающую программу — в неработающую

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


Вы написали чушь собачью.

Хороший аргумент. Пожалуй, на этом стоит закончить.

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

Что за философские вопросы?
Это не «философские вопросы». Это набор «правил игры». Если конструкция имеет некоторое определённое поведение, то, стало быть, есть некоторые ожидания от того, что будет делать программа в которой такая конструкция встретилась. Если же поведение не определено — то никаких ожиданий нет. Любое поведение — будет ожидаемым. Без каких-либо ограничений.

В этом сама суть неопределённого поведения. Оно потому и не «неопределённое», что никаких ожиданый у вас по поводу такой программы быть не может.

Компилятор про него ничего не знает,
Как это не знает?
у него есть только исходный код.
И в этом коде — есть функция, которая, из другого потока, меняет ebx. Так что знает он всё, конечно — вопрос только в том, что использовать это знание, как бы, проблематично.

Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.
Совершенно верно. Есть volatile, есть библиотека атомарных операций и вообще есть много разного-всякого, что позволяет вам проделывать подобные штуки не нарываясь на UB. И вот только и только тогда, когда ваша программа написана так, что она не вызывает UB — вы можете иметь какие-то «ожидания».

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

Вам юристом бы стоило пойти работать. Это у них всё всегда зависит от контекста и нет никаких «жёстких» правил игры, случае буквально до буквы совадающие могут быть трактованы по разному из-за каких-то совершенно мелких тривиальностей…

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

Что там и как будет у программ, вызывающих UB — их не волнует от слова «совсем».

Пожалуй, на этом стоит закончить
Ну дык. Вы уже сдали половину своей позиции и перешли от "Написано, значит должно появляться, или не компилироваться" к "Надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока»". Если мы не остановимся, то ещё через день-другой до вас, возможно, наконец дойдёт, что и разделать эти понятия, в общем, не нужно — нужно просто по другому по другому рассклассифицировать некоторые случае в стандарте и перенести их из категории неопределённого поведения в категорию неуточняемого поведения. С чем, как бы, особо никто и не спорит. Вспомните мою реплику: Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…
То есть в компилятор должен быть встроен ещё и модуль телепатии, который будет читать в голове у автора его ожидания?

Нет. У компилятора есть исходный код.


и перешли от "Написано, значит должно появляться, или не компилироваться"

Нет. Эта фраза относилась к случаям вида "UB из-за неинициализированной переменной" и конкретно к примеру, где компилятор заменил одну проверку на другую.


Впрочем ладно. Я попытался объяснить, но у меня не получилось. Будем считать, что я не прав.

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

в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).


в C# есть implementation-specific behavior, плюс unsafe.


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

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

Допустим — проверка границ массива. В C/C++ — это UB, Java/C# лепят рантаймовые проверки. Вполне ощутимый оверхед, зато позволяет точно сказать, что будет, если «промахнуться». Всякие точки следования, порядок вычисления операндов — аналогично позволяют оптимизировать вычисления в ущерб некоторым гарантиям.
в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).
Не только этим. В языках без GC избавиться от UB черезвычайно сложно. А добавить в C++ GC или borrow checker — это уже совсем-совсем другой язык получится.

Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…
В языках без GC избавиться от UB черезвычайно сложно.

Я бы сказал, что GC здесь идёт параллельно.
А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.

А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.
При ручном выделинии и исвобождении памяти UB неизбежен, так как нет возможности убедиться, что в момент free «живых» указателей на этот участок памяти больше нет. А если можно — то зачем этот free, собственно, нужен?

Скажем в Ada (суровый такой аэрокосмический язык) прямого доступа в память нет, а UB — таки да. Именно из-за отсуствия GC. Ada с GC — UB уже не имеет, там всё жёстко определено.

Не совсем так. Если напутать с освобождением памяти — будет повреждение кучи. Ситуация неприятная — но все же не настолько как UB.


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


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

Ситуация неприятная — но все же не настолько как UB.
Вообще-то обращение к обьектам, которые были удалены — один из вариантов UB. Так что неясно что эта фраза вообще должна значить.

UB же способно испортить программу еще до того как произойдет — и такие баги ловятся только плясками с бубном или изучением ассемблерных листингов.
Нет, нет и нет. 100500 раз нет. Программа может содержать сколько угодно потенциальных UB, однако если при её конкретном запуске никакого UB не случилось — то она обязана работать.

Иначе вообще ничего нельзя было бы на C/C++ написать в принципе, ибо, как уже говорилось любое i++ — это, потенциально, UB.

Если вы найшли UB, который что-то испортил до того, как произошёл (так бывает, хотя и очень редко) — это повод «трубить во все колокола», файлить баги на компилятор и т.д. и т.п. Этого быть не должно, точка.

Вот вам пример:


if (a) printf("a is not null\n");
b = a->c;

Если оптимизатор докажет, что переменная a не может измениться в результате printf — он может выкинуть проверку. В итоге, когда a все-таки окажется нулевым указателем, получится что UB "проявилось" до того как произошло — что значительно затруднит отладку.


Или я не прав?

Нет, так компилятор делать не может. Вот если переставить строки местами — тогда да, тогда компилятор может выкинуть проверку, а после этого, подняпрягшись, может выкинуть и b тоже, в результате чего, скажем, ваша программа перестанет правильно реагировать на nullptr (я приводил законченный пример в другом комментарии).

Однако всё это происходит после точки, где вы вызвали UB! То есть да, UB может приводить не только к тому, что ваша программа станет делать что-то плохое, но и к тому, что она перестанет делать что-то плохое… но всё это происходит после того, как она, условно говоря, «пересекла» точку невозврата.

P.S. Есть правда, исключение: компилятор может модицицировать код, который не вызывает побочных эффектов и тогда, в некотором смысле, UB будет распространяться «назад по коду» — но это и без всяких UB может происходить.
А что тут комментрировать… Да, язык ужасно сложный и коварный. Но уж какой есть. Приходится использовать вспомогательные инструменты, такие как PVS-Studio.

Сам то я только рад такому положению дел. Это приносит нам деньги. :)

Выдавать предупреждения надо очень аккуратно. И многие предупреждения мы не делаем именно по той причине, что будет слишком много ложных срабатываний (см. Философия статического анализатора PVS-Studio). А когда делаем какие-то диагностики, то стараемся учесть эмпирические нюансы, которые подсказывают есть ошибка или нет.
Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте
Фразе больше 20 лет. С неё и надо начинать учить программирования. А за code-ниндзюцу, приведенное в статье, надо прищемлять пальцы дверью, что бы больше такого написать не смог уже никогда. Ну, если это не «патч Бармина» и тому подобные вещи — но они и сделаны для того, чтобы их сложно было прочитать.
Если мой ЯП строго определяет порядок вычислений и всё описанное в статье чётко определено — почему это означает то, что я пишу говнокод?
  1. Не все знают стандарты наизусть. Более того, однажды выучив, через какое-то время удивишься, что часть уже забыл или исказил;
  2. Нужно думать гораздо больше, чем ожидается от среднего по больнице;
  3. Отладчики работают почти всегда построчно, а если и есть возможность по операторам, то сделать это проблематично в плане интерфейса — неудобно;
  4. Порядок выполнения задает номер строки или скобочки, но никак не стандарты — вот правило, которое должен знать каждый программист.
Порядок выполнения задает номер строки или скобочки, но никак не стандарты.

Я не распарсил.
Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу и, разуеется, не нужно менять в одном и том же выражении одну и ту же переменную дважды.
Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.
Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.

Но, например, в Паскале порядок операций достаточно простой — и нету писать «1+(2*3)», «чтобы было понятней».

Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (аля сложения и умножения) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
Считать коммент удалённым.
Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.
Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.

Но, например, в Паскале порядок операций достаточно простой (а в третьем языке ещё логичнее) — и нету смысла писать «a + (b * c)» лишь «чтобы было понятней».

Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (ал-я сложение и умножение) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
Не на тот комментарий ответил, см. ниже.
Стандарты могут меняться.
Ваш код возможно будет поддерживать или читать человек не знакомый с деталями стандартов.
Ваш код возможно будут портировать на языки имеющие иную особенность реализации подобных выражений.
Разные разработчики по разному трактуют использование подобных выражение. Упрошенное выражение сможет прочитать любой разработчик, сложное выражение только регулярно его использующий, остальные полезут в доки.

Порядок выполнения задает номер строки

Лучше без нужды на это правило не полагаться.
UFO just landed and posted this here
Порядок вычисления выражений...

Некоторые языки программирования (например, Cache ObjectScript) приучили меня расстаалять скобки даже там, где они не нужны.
Вроде такого: 2+(2*2)

Теперь, допустим, что вы знаете (и эти знания компилятору недоступны), что метод подсчета налога calculate_tax обновляет значение локальной переменной налог (tax) в объекте, но не изменяет базовую цену; итак, для вас это предупреждение будет ложной тревогой.

Вот только правила проектирования говорят нам, что мы не должны писать код на основе знаний о внутреннем поведении. Ибо поведение изменится и всё сломается.
В теории — да.
На практике возможности задания сигнатур функций в большинстве современных языков программирования достаточно убоги (например, мы не можем прямо в сигнатуре функции сказать «вот эти поля объекта this она имеет право перезаписывать, эти — не имеет, а эти — даже читать не имеет права»). Остаётся писать такое в документации; или полагаться на здравый смысл, если документации нету.
Хотя, конечно, можно взять за правило считать «на всё, что не написано прямо в объявлении функции или в официальной документации, полагаться нельзя» (но и с этим может быть сложно).
А если спросят на собеседовании?
Сказать, что разработчики С# тоже не знают ответа на этот вопрос?

Сказать, что вы такого кода писать не будете. А если встретите чужой — то поведение всегда можно увидеть в отладчике.

Т.е. сознаетесь в некомпитентности?

У меня знакомый когда устраивался С++ разработчиком после 3-4 собеседований учил нечто вроде
for(int = i;i-- < 6;++i++)

Я бы не стал работать в таком месте. Только идиот будет писать такой код.
А здесь будет error: lvalue required as increment operand

Т.е. сознаетесь в некомпитентности?

Всё знать невозможно и не нужно.
Фундаментальные алгоритмы знать — это одно. А уметь писать/читать скрижали — другое.
Т.е. сознаетесь в некомпитентности?

Компетенция программиста — навык писать поддерживаемый код. Учить это смысла нет вообще никакого, хотя для C++ стоило бы поинтересоваться, почему так писать неправильно.
А можно тупой вопрос? Вот этот вот оператор, ++, он вообще в нынешних реалиях, зачем нужен? Ведь по сути это такая примитивная лямбда.
Моё мнение.
  1. Обязательно должен быть оператор взятия следующего/предыдущего элемента. Не меняющий значение операнда, а просто возвращающий следующий/предыдущий элемент. То есть не каждый тип данных будет поддерживать операторы «a + целочисленное_значение» и «a — целочисленное_значение», но операторы следующего и предыдущего элемента будут поддерживаться бо́льшим числом типов (но если тип поддерживает «a + целочисленное_значение»/«a — целочисленное_значение», то на программиста должен накладываться контракт, что «a + 1»/«a — 1» делают то же, что и операторы следующего/предыдущего элемента).
  2. Опционально могут быть предусмотрены операторы, сокращающие записи вида «a ← следущий_элемент_от a» и «a ← предыдущий_элемент_от a». То есть пребывающие с описынным(и) в предыдущем пункте оператором/-ами в таком же отношении, в котором Си-шный «+=» пребывает с «+», Си-шный «*=» — с «*», Си-шный «%=» — с «%» и т.д. Да, они могут записываться имено как «a++», «a--» (выбор Go), а могут и как-то по-другому (например, в зависимости от того как именно выглядят операторы следующего/предыдущего элемента из предыдущего пункта и коррелировать с тем, как в языке записываются, если есть вообще, операторы вида «a ← a какойто_инфиксный_оператор b»). Но:
    • Формы, возвращающие старое значение — нафиг. Для операции «a %= b» в Си нету аналогичного оператора, возвращающего значение a, которое было до применения %, значит и для «++a» в Си не должно быть оператора, возвращающего значение a, которое было до применения оператора взятие следующего элемента. (Ну или наоборот — для всех делать, а не только для «++a» и «--a».)
    • Эти операторы должны мочь использоваться в выражениях (а не только в инструкциях) тогда и только тогда, когда операторы присвоения («a ← b») могут использоваться в выражениях (а не только в инструкциях). Например, в Си «a = b» — выражение (возвращающее новое a), значит и эта операция должна быть выражением (возвращающим новое значение). В Паскале (Go) «a := b» («a = b») — инструкция (не выражение), значит и эта операция — инструкция (не выражение).


Си с треском проваливает оба пункта.
Go имеет второй, но проваливает первый.
Паскаль имеет первый, но не имеет второй.

То есть вы предлагаете в Си запихать итераторы из C++ STL?

Ума не приложу, откуда Вы это взяли.

Если Вы про то, что в Си++, начиная с версии 11, появились функции std::next и std::prev — то это всего лишь методы (а не операторы).

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


функции std::next и std::prev

Да зачем, ::iterator + 1 даст следующий элемент контейнера, будь он вектором, словарем или еще чем-то.

Не даст, например, для списка.

Но даже не в том дело. Просто, очевидно, система неполная, некрасивая и кривая. Рано или поздно это аукнется.
В циклах for хорошо смотрится. Лучше чем лямбда.

Мне нравится, как это решается в Python через enumerate() если реально нужно знать индекс элемента.
А если его знать не нужно foreach (или ranged for в 11х плюсах если не залезать в STL) — наше все.

Этот код вызовет предупреждение: компилятор увидит, что метод calculate_tax не является константным (const), поэтому он обеспокоится тем, что метод может изменить переменную base_price — и в этом случае иметь значение будет то, считаете ли вы налог по оригинальной base_price базовой цене, или по уже измененной.


Этот код вызовет предупреждение (если вызовет) в не зависимости от того помечен ли метод calculate_tax константным или нет. Компилятору вообще практически всегда наплевать на ваши const определения.
UFO just landed and posted this here
«Чтобы все офигели как я могу».
В моем рабочем опыте это обычно показатель законченного эгоиста, который не заботится о том, как код будут читать другие программисты.
Sign up to leave a comment.

Articles