Порядок вычисления выражений определяется конкретной реализацией, за исключением случаев, когда язык гарантирует определенный порядок вычислений. Если же в дополнение к результату вычисление выражения вызывает изменения в среде выполнения, то говорят, что данное выражение имеет побочные эффекты.В нашей внутренней рассылке про C# регулярно возникает дискуссионный вопрос, который касается корректной интерпретации подобных конструкций:
MSDN
a -= a *= a;
p[x++] = ++x;
В ответ я спрашиваю:
Да кто вообще пишет такой код с невозмутимым видом? Одно дело, когда такое пишешь, пытаясь победить в «Международном Конкурсе запутывания кода на Си» (IOCCC, International Obfuscated C Code Contest), или если хочешь написать головоломку — но в обоих случаях понимаешь, что ты занимаешься чем-то нестандартным. Что, реально есть кто-то, кто пишет a -= a *= a и p[x++] = ++x; и думает про себя «Чёрт возьми, да я пишу действительно классный код!»На что Эрик Липперт отвечает мне: «Да, такие люди определенно встречаются». В качестве примера он привел одну успешную книгу популярного автора, который свято верил в то, что чем короче код, тем быстрее он работает. Так вот, представьте себе — продажи этой книги составляют уже свыше 4 миллионов копий и продолжают расти. Автор этой книги постарался впихнуть в каждое выражение несколько побочных эффектов сразу, плотно усеяв их условными тернарными операторами; всё дело в том, что он искренне верил в то, что скорость выполнения программы пропорциональна количеству использованных в ней точек с запятыми — и что каждый раз, когда программист объявляет новую переменную, Бог убивает щеночка.
Разумеется, приложив некоторые усилия, можно научить компилятор анализировать такой код и выдавать предупреждение вроде такого: "Результат этой операции может различаться в зависимости от порядка вычисления". Однако, в случае принятия подобного решения, вам придется иметь дело с другими проблемами.
Во-первых, будет большое количество ложных срабатываний. Допустим, вы напишете следующий код:
total_cost = p->base_price + p->calculate_tax();
Этот код вызовет предупреждение: компилятор увидит, что метод calculate_tax не является константным (const), поэтому он обеспокоится тем, что метод может изменить переменную base_price — и в этом случае иметь значение будет то, считаете ли вы налог по оригинальной base_price базовой цене, или по уже измененной. Теперь, допустим, что вы знаете (и эти знания компилятору недоступны), что метод подсчета налога calculate_tax обновляет значение локальной переменной налог (tax) в объекте, но не изменяет базовую цену; итак, для вас это предупреждение будет ложной тревогой.
Как вы уже увидели сами, проблема здесь в том, что в случае добавления подобного предупреждения нас ждёт ужасно большое количество ложных срабатываний — в результате чего разработчики будут просто отключать это предупреждение.
Что ж, хорошо, сделаем шаг назад, и давайте будем предупреждать только о самых вопиющих случаях — только тогда, когда переменная изменяется и вычисляется в одном и том же выражении. "Предупреждение: Выражение полагается на порядок вычисления".
Возьмём супер-умного программиста Эксперта Джо: он знает, что его код безупречен, а компилятор — слабак. «Хорошо, да это же очевидно, что сначала делается инкремент переменной, затем она используется для вычисления индекса массива, а затем результат поиска в массиве сохраняется обратно в переменную. Здесь нет конфликта порядка вычисления. Тупой компилятор!». В результате супер-умный программист Эксперт Джо отключит это предупреждение, сочтя его бесполезным. Что ж, наш Эксперт Джо — это всё-таки безнадежный случай, и за него мы не беспокоимся.
Но возьмем другого программиста, Новичка Джо — на деле, он даже не поймёт сути этого предупреждения. «Ну ок, давайте посмотрим. Я компилировал эту функцию пять раз, и каждый раз я получал одинаковый результат. Результат выглядит для меня надежным. Похоже на то, что предупреждение было ложным». Таким образом, как раз те, кто должен был получить пользу от этого предупреждения, не всегда обладает достаточными знаниями, чтобы понять его.
Конечно же, проходит некоторое время, и этот вопрос всплывает в рассылке вновь. Кто-нибудь обязательно спросит, почему выражение x ^= y ^= x ^= y не работает в C#, хотя работает в С++. Вот вам ещё одно доказательство того, что некоторые всё-таки пишут код, который полагается на несколько побочных эффектов сразу — и эти же люди искренне считают, что их код очевиден и гарантировано будет работать.
Ссылка на оригинал
Ссылка на обсуждение
Комментарии (277)
nazarpc
23.07.2017 17:44+39Для меня правило хорошего тона всегда брать в скобки и выносить на отдельную строку/в отдельную переменную то, что вызывает повышенное шевеление извилинами в во всём остальном обычном коде. Не вижу никакого смысла постоянно напрягать память для того, чтобы вспомнить каким именно образом компилятор или интерпретатор обрабатывает тот или иной уникальный случай. Проще написать больше тупого кода, чем меньше излишне умного. Сложно придумать аргументацию против такого подхода.
Хотя в последнее время читая код весьма продвинутых программистов вижу на удивление большое количество без необходимости сокращенных названий переменных, невыразительных названий функций/методов и полное отсутствие малейшего комментирования происходящего. Может это я такой тупой, но всё же не вижу никакого смысла в сокращении privateKey до pk и прочих подобных, ибо код превращается в кроссворд из кучи переменных каждая из которых содержит максимум 3 символа, а чаще вообще 1.
A-Stahl
23.07.2017 18:23+1>а чаще вообще 1.
Но-но-но! Имена некоторых однобуквенных переменных уже стали нарицательными. Попробуйте обозначить координату не через x,y,z или счётчик не через i,j,k и вы увидите много непонимающих взглядов:)nazarpc
23.07.2017 18:26+5Я имел ввиду гораздо менее понятные сокращения) Против указанных в общем случае не имею ничего против.
vlanko
23.07.2017 20:56+5я работаю сейчас с географическими/прямоугольными координатами. Там х и у наоборот. И часть библиотек написано математиками(х вправо, у вверх), а часть геодезистами(наоборот). Боюсь, мне захочется назвать их сложнее :))
А z там есть, но спрятан.radium
23.07.2017 22:36+1О как это знакомо! Вероятно это будет fi и la (? и ?) или lat и lon.
khim
23.07.2017 23:22Вероятно это будет fi и la (? и ?) или lat и lon.
А почему не ? и ??Krypt
24.07.2017 00:04+5Можно и ? и ?, если живёте где-нибудь в Афинах и у вас на клавиатуре есть эти символы. Правда я думаю, что это будет равносильно написанию названий переменных на русском у нас.
radium
24.07.2017 00:14+5Если мы говорим о мейнстриме (C++/C#/Java), то общая практика состоит в избегании применения не ASCII символов в коде. До сих пор бывают проблемы с кодировкой исходников, что приводит к нежелательным последствиям. А ещё можно представить, что потом эти названия полезут в некое публичное API. А это накладывает дополнительное требование поддержки не ASCII-символов на все инструменты, которые с этим API работают, будь это линкеры для библиотек или OData/REST кодогенераторы для WebApi. Слишком большая проблема для ровного места :)
Alyoshka1976
23.07.2017 23:30+8Забавный пример из личной практики, связанный с именами переменных — когда в конце 80-х я осваивал программирование на МК-61 и МК-52, то одной из моих любимых книжек была книжка небольшого формата Очкова и Хмелюка «От микрокалькулятора к персональному компьютеру». Там были примеры программ как для микрокалькуляторов, так и на Бейсике. Меня в то время долго мучал вопрос — почему во многих программах фигурирует переменная с загадочным именем TEMP, никак не связанная с температурой ))) (cитуация усугублялась тем, что в школе я учил немецкий).
TerraV
24.07.2017 13:26-6i, j и k это зло. В циклах должны быть нормальные переменные типа rowIndex, columnIndex и т.п. Два вложенных фора c i/j очень часто на этапе написания содержат ошибку. И даже если ошибки нет, время потраченное на осмысление можно использовать более продуктивно.
Free_ze
24.07.2017 14:28+6Два вложенных фора c i/j очень часто на этапе написания содержат ошибку.
Прокладку, между стулом и клавиатурой (=
Значение этих переменных очевидно настолько, что замена мнемониками пользы не принесет. Есть такое правило: чем меньше скоуп жизни переменной, тем короче имена. Если в таком цикле возможно даже i с j перепутать, то тут уже стоит подумать о том, как его отрефакторить.sasha1024
24.07.2017 14:42+1А я согласен с TerraV.
Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c, а не a, b, c, d (и не i, j, k, l). Если одной буквы не хватает для уникальной идентификации (например, columns и cells, или tests и testTargets) — двумя или даже полными словами.Free_ze
24.07.2017 15:23+4Тут действительно большая разница между i,j,k и хитромудрыми a,b,c,d, о семантике которых любой бы задумался. for и i — это как хлеб и рама.
sasha1024
24.07.2017 15:56Вы, наверно, имели в виду «хитромудрыми a, u, r, c».
Free_ze
24.07.2017 16:29+4Их тоже, это не имеет значения на самом деле) Если в контексте имя переменной не дает стойкой ассоциации (как, например, r в геометрической формуле), то лучше воздержаться.
Я делаю имена итераторов и счётчиков цикла короткими, но мнемоническими. Например, итераторы/счётчики для addresses, users, rows, columns будут называться a, u, r, c
Использование i,j,k в качестве индексаторов — это общепринятая практика, а однобуквенные штуки намекающие на тип будут уместнее в foreach-циклах, где это не просто индекс, а экземпляр, обладающий состоянием и поведением.Lex20
25.07.2017 20:37-2Экземпляр, обладающий состоянием и поведением? Похоже на унижение школьника учителем.
Free_ze
26.07.2017 10:18+2Нет, это не пикабу, здесь сидят специалисты, владеющие терминологией.
Lex20
26.07.2017 19:44-2Этой терминологии всего несколько десятков лет, она вполне может поменяться. Меня же коробит что используют неочевидные языковые конструкции, наталкивающие на неправильные мысли. Я бы написал так: это не просто индекс, а элемент структуры данных. Статья именно об этом — о неочевидности кода.
Free_ze
27.07.2017 11:16+3Фундаментальные понятия имеют интересное свойство: меняться либо редко, либо никогда.
Меня же коробит что используют неочевидные языковые конструкции, наталкивающие на неправильные мысли
Любая наука обрастает своей профессиональной терминологией, что поделать?.. Или вы предлагаете «хреновинами» и «фиговинами» оперировать? (=
Если для вас это неочевидно, то это печальненько, ибо рассказывают это студентам на первом курсе.
Я бы написал так: это не просто индекс, а элемент структуры данных.
Сложными словами я как раз подчеркивал, что индекс != элементу коллекции, это всего лишь подярковый номер, без «состояния и поведения».
DistortNeo
24.07.2017 15:35А если это шаблонный метод?
sasha1024
24.07.2017 16:02+1И? В чём разница?
А, я понял. Вы вспомнили про ту глупую традицию называть параметры шаблонов T, T1, T2, …? Ну так это тоже нафиг.
Параметры шаблонов должны иметь осмысленные имена. Стилизания их названий должна быть такая же, как и стилизация того, что они обозначают (если параметр-тип и типы мы пишем с большой буквы, то с большой; если параметр-константа и константы мы пишем так-то, то так-то). MySuperCollection<Item> (или на худой конец MySuperCollection<ItemType> — хотя это спорно, мы ведь называем Integer и CustomerInfo, а не IntegerType и CustomerInfoType — ну да ладно), но не MySuperCollection<T>.DistortNeo
24.07.2017 16:07И? В чём разница?
Есть некая
someFunction<TInput, TOutput>
, где в качестве TInput и TOutput может быть ну совершенно что угодно. Какие буквы вместоi
иj
вы предложите для переменной-индекса?
vbif
24.07.2017 15:47+2А вот этого я не понимаю. Ладно i/j/k означают не более чем «число от 0 до x», но зачем сокращать итераторы?
Другое дело — функции, делающие что-то тривиальное, когда кроме «v1», «v2» сложно выдумать что-то подходящее. «firstValue/secondValue»?sasha1024
24.07.2017 16:09-1А в чём принципиальная разница между целочисленными счётчиками цикла и итераторами — не понимаю.
vbif
24.07.2017 16:41+1Огромная. В том, что здесь мы имеем дело не с абстрактным «числом от одного до десяти», а с конкретным элементом конкретного списка. И логично элемент списка «addresses» назвать «address», «users» — «user» и т.д.
Free_ze
24.07.2017 16:46Итератор — это, как правило, объект, позволяющий получить доступ к ячейке контейнера. Например, итератор позволяет вам перемещаться между узлами связного списка, где инкремент счетчика бесполезен.
mayorovp
24.07.2017 19:08+1Дело в том, что целочисленные индексы используются всегда совместно с коллекцией:
users[i]
. При этом вся необходимая для понимания семантики информация уже сосредоточена в имени коллекции.
Итератор же используется как самостоятельный объект, будучи синтаксически оторван от связанной с ним коллекции.
zenkz
24.07.2017 17:35+4Зря минусите человека.
i,j,k — вполне применимы в простых случаях без вложенных циклов, хотя и не помню когда последний раз их использовал, т.к. легко заменяются на foreach.
А вот при работе с таблицами я бы предпочёл видеть rowIdx и colIdx — чуть больше писать, зато позволяет избегать детских, но труднонаходимых ошибок в коде.
KoCMoHaBT61
23.07.2017 18:47-2Не так давно разбирался в одноразовой функции getAndConvertPhysicalToLogicalValue и нихрена не понял откуда она и что берёт. В результате функция вырезана нафиг, а логика работы (то, что функция должна была делать) перенесена в тело цикла. При этом пропала передача параметров и прочие сопутствующие причандалы.
Где-то есть золотая середина.radium
23.07.2017 22:45+17нихрена не понял откуда она и что берёт
Т.е. функция была сложна и непонятна?
логика работы (то, что функция должна была делать) перенесена в тело цикла
Т.е. тело цикла и функции, в котором он находится, стало ещё более сложным и непонятным? А в чём профит Вашего действия?KoCMoHaBT61
24.07.2017 08:40Функция была странная, а термины «Logical» и «Physical» в этом контексте никогда не употреблялись.
Тело цикла стало больше, но в силу того, что параметры никуда не передаются можно отследить логику работы.
sumanai
24.07.2017 16:46+3getAndConvertPhysicalToLogicalValue
Вот такой фигни не должно быть, функция должна выполнять одно действие. И это безотносительно того, что непонятно, что за значения такие.KoCMoHaBT61
24.07.2017 17:18+1Согласен на 100%.
Функция вызывалась один раз, называлась длинно с использованием непонятных терминов, передавала всякое туда-сюда по значению.
Заменилась на три строки в теле цикла.Color
24.07.2017 17:34+1Если бритва Оккама позволяет отсечь эту функцию, то вполне норм от нее избавиться, считаю.
Имеет смысл запаковывать логику в методы, когда это действительно имеет смысл (вот такая тавтология) — то есть, если там предполагаются изменения, либо она относится к какой-либо отличной зоне ответственности, нежели вызывающий метод, либо имеет место DRY, либо еще что.
Если там дествительно три строки, да и еще название не соответствует, возможно, там когда-то раньше было больше, а потом разработчик это убрал, да поленился остатки перенести назад или переименовать нормально. Избавление от старого хлама есть часть рефакторинга.
Ну а золотая середина (имхо) в том, чтобы придерживаться четких принципов и методологий ака SOLID, GRASP и прочих, которые как раз помогают четко детерминировать границы когда "так" делать, и когда "так" не делать.
Alex_ME
23.07.2017 19:45+4Комментарии в чужом коде — большая моя боль. Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?
Сам я оставляю много комментариев, возможно, слишком много. Стараюсь писать что делает какой-то кусок кода и с какого перепуга я написал его именно так, если это не очевидно. Во-первых, легче разобраться, а к тому же IDE комментарии подсвечивает и они очень наглядно отделяют блоки кода. В коде без комментариев даже глазу не за что зацепиться.
nazarpc
23.07.2017 19:48+6Зависит от того, как ещё переменные и методы обозваны. При хорошему подбору названий необходимость в комментариях резко уменьшается.
d-stream
23.07.2017 19:54+1Главный критерий достаточности количества комментариев — чтобы сторонний человек не реагировал как Alex_ME (собственно я так же реагирую)
SirEdvin
23.07.2017 20:01+2Проблема в том, что иногда не понятно, что писать в методе. Он часто говорит сам за себя, если вы находитесь в контескте логики приложения.
ozonar
24.07.2017 09:58+2Это он в момент когда ты его пишешь говорит сам за себя. Через полгода или просто если его другой человек откроет, уже будет сложно понять и контекст и смысл метода.
Я работал с программистом, который делал метод, к примеру, «FindMemo», и оставлял комментарий: «Файндит мемо». Несмотря на то, что добавить ему было нечего, понятней этот кусок кода не становился.mayorovp
24.07.2017 10:04+1Так о том и речь — что добавление комментария не добавляет смысла. Если метод назван понятно — то и комментарий не нужен. Если метод назван непонятно — то и комментарий будет таким же непонятным :-)
ozonar
24.07.2017 10:11+4Дак я не о том =)
Опять же, если взять мой пример с «FindMemo», рядом есть дефолтный метод Find. Для чего нужно было делать кастомный, и что такое memo в текущем контексте можно узнать только если детально разобраться в методе. Эту информацию и нужно было оставить в комментарииmayorovp
24.07.2017 10:22+1Вот только вместо нормального комментария был оставлен комментарий "Файндит мемо". Почему? Потому что программист думал что это понятно.
Чтобы написать правильный комментарий, нужно было чтобы программист осознал что "Файндит мемо" — непонятно. Но в таком случае что помешало бы ему и метод тоже назвать по-другому?
aamonster
24.07.2017 11:19Обычно надо не метод переименовывать, а подробней описать аргументы и результат (ну, как обычно описаны функции у ms/apple/..., и в doxygen/… удобно делать)
MacIn
24.07.2017 12:48То, что в этом случае метод имел бы имя длиной символов в 50. Для комментария-то это нормально…
khim
24.07.2017 13:43+1И для функции — тоже нормально, если короче понятное название не придумывается. Важно же, чтобы в том месте, где функция вызывается было понятно что она делает — а там вашего чудного комментария уже и не будет.
quantum
24.07.2017 14:54+1На помощь приходит ide, которая в подсказке указывает описание метода и параметров
khim
24.07.2017 15:03-1Вы читаете код, тыкая в каждую функцию и читая описание метода и параметров? Мне вас жаль.
На практике код приходится читать гораздо чаще, чем писать, так что важно оптимизировать именно скорость чтения кода.
sasha1024
24.07.2017 15:14Сталкивался со случаями, когда мне говорили: «Непонятное название — укороти.»
Категорически не согласен с таким — слишком длинное (и написанное на грамотном английском) название может быть неудобным в использовании или ещё что-то, но не непонятным.
Жаль, что существующие языки/IDE не предлагают вменяемых механизмов сокращения названий функций (типа, вообще она называется длинно и в достаточно удалённых модулях её будут называть полностью, но в нашем модуле, имеющем с этим набором функций дело часто, мы будем называть их так-то, так-то и так-то, а не полностью).michael_vostrikov
24.07.2017 20:38use function cos as c; echo c(0); // 1 var getById = document.getElementById; console.log(getById); // function getElementById() { [native code] }
MacIn
24.07.2017 20:13Надо смотреть по каждому конкретному случаю. Если имя функции такое длинное, оно не будет влазить с параметрами в строку.
Free_ze
24.07.2017 15:28Методы не висят в воздухе, а лежат внутри типов (инстансы которых также могут иметь имена), реализующих интерфейсы, которые находятся в пространствах имен. Всё это уточняет контекст.
А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP. (Было бы неплохо посмотреть на реальный пример)MacIn
24.07.2017 20:15+1Ок, пусть это не метод, а просто функция.
А длинные имена (едва ли там три слова дают в сумме 50 символов) говорят о том, что метод скорее всего нарушает SRP.
Допустим, некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.Free_ze
25.07.2017 11:59Имена свободных функций обычно длиннее. Ну ОК, это не сильно меняет суть.
некая функция извлекает элемент определенным образом, так что просто ExtractItem недостаточно внятное название.
Нужен какой-то хороший пример. Что за айтем, откуда мы его берем? Если добавится еще 2-3 слова, то ничего страшного в этом нет. В названии нужно описывать цели, а не алгоритмы.
vsapronov
24.07.2017 16:47+1Есть такая штука: разбиение метода на два и более.
Если вам хочется назвать метод более чем 3-мя словами, то либо у вас словесный понос, либо ваш метод делает слишком много одновременно и нарушает хотя бы принцип единственной ответственности (single responsibility).
Можно, конечно, обложить его комментариями, если это позволяет заснуть без угрызений. Но Мартин рекомендует выделить метод: Extract Method. И тогда методов бодет больше, они будут проще и не нуждаться в коментарии, потому что все что можно написать в коммент будет в коротком названии метода.michael_vostrikov
24.07.2017 17:34+2… и будет нифига непонятно, как они работают в целом) Надо все-таки соблюдать баланс.
vsapronov
24.07.2017 18:35Надо смотреть на конкретный пример кода. Как правило, в подобных разговорах о коде теоретически можно зайти в далекие от реальности предположения.
Почему я это говорю? Потому что вы вот говорите «будет нифига непонятно» — да, соглашусь я, возможно, будет нифига не понятно, но главная причина будет в другом — говённо написанный код.
В нашем разговоре мы хотим понять как методы «они работает вцелом» — это значит, что есть какое-то еще новое место в коде, которое вызывает эти маленькие функции. И вот об этой «большой» функции мы и будем говорить — нужен ей комментарий или нет?
Вот например пример такого «большого» метода, в котором маленькие «работают вцелом»:
public byte[] ReadData (string url) { var connection = this.OpenConnection(url); var data = connection.ReadData(); connection.Close(); return data; }
Даже если добавить обработку исключений, то все равно понятно при первом взгляде, что код открывает соединение, читает данные, закрывает соеднинение.
Что мы можем написать в комментарий?
// Opens connection, reads data, closes connection
Это легче, чем просто просмотреть код метода?
Всегда можно и нужно обсуждать насколько большими должны быть «большие» методы.
Не нравится пример — приведите свой.
Я не говорю, что комментарии не нужны вообще, я говорю, что в 95% случаев с которыми я сталкивался комментарии — неудачная попытка объяснить говённый код. И вместо того, чтобы плодить неконтролируемый компилятором, быстро устаревающий текст, нужно подумать о дизайне кода и исправить его.michael_vostrikov
24.07.2017 18:53Тут комментарий и не нужен. Я имел в виду, что при вынесении в функцию теряется контекст, откуда и при каких условиях она вызывается. Примера под рукой нет, просто иногда такие случаи встречались. В основном, это были какие-то функции для частных случаев, которые вызываются в теле цикла или внутри сложного условия.
MacIn
24.07.2017 20:22+1Есть такая штука: разбиение метода на два и более.
Если вам хочется назвать метод более чем 3-мя словами, то либо у вас словесный понос, либо ваш метод делает слишком много одновременно и нарушает хотя бы принцип единственной ответственности (single responsibility).
Не в этом дело.
Это редкие случаи, но вполне реальные. Я сейчас не буду искать по коду эти редкие случаи, вот синтетический пример: у вас есть метод, извлекающий из коллекции элемент по некоему идентификатору целочисленного типа. И еще один метод, извлекающий элемент по целочисленному идентификатору, но другому.
Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID
Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови. Более того, придется называть наоборот, более многословно, чтобы пояснить, что же именно вот этот абстрактный оторванный от жизни кусок делает; тогда как в норме это была бы часть другого, более крупного метода и не нуждалась бы в пояснениях ни через комментарии, ни через название в силу наличия контекста.vbif
24.07.2017 22:42Можно выбирать идентификатор через замыкание. Можно передавать параметр, который будет выбирать идентификатор. Можно придумать множество других способов.
vsapronov
25.07.2017 00:17В любом случае, мы уже сходимся в том, что «это редкие случаи». Поэтому ожидать, что каждый метод и функция будут снабжены комментариями — неправильно. Должны быть откоментированны только некоторые «редкие случаи», в которых должно быть ясно, почему невозможно дальнейшее улучшение дизайна с целью улучшения читаемости…
Bonart
25.07.2017 02:00Если разбивать методы дроблением до совершенно малых, то теряется логика работы, как хорошо их ни назови.
Не теряется, если разбивать по SRP.
michael_vostrikov
25.07.2017 09:48-1Тоже думаю, что теряется, так как в пределе получается ассемблер.
Bonart
26.07.2017 02:07В пределе разбиения ассемблер получиться не может так как речь о выделении ответственности, а не о низкоуровневой оптимизации.
michael_vostrikov
26.07.2017 06:40-1Я не про оптимизацию, а про структуру в целом. Много мелких операций.
Free_ze
25.07.2017 12:04Соответственно, у ва сбудет метод ExtractItemByID (например), и, условно, ExtractItemByOriginalID
Что с ними не так?
SirEdvin
24.07.2017 11:04+1Это довольно сложный вопрос. Если ваш программный продукт построен на фреймворках и там нет ни одной общей строки — тогда да.
Или вот, например, бывают такие методы.Возможно не очень понятно, что он делает сам по себе, но если вы находитесь в контексте работы приложения (а именно, это бот для работы с чатами), то в целом становится понятно.
Проблема в том, что если вы в целом не понимаете приложение или не находитесь в его контексте, то комментарии вам и не помогут. А если находитесь в контексте, то бывают и не нужны. Понятное дело, если пишется какой-то кастомный метод по какой-то причине, то лучше ее указать, но в целом довольно сложно писать комментарии к каждому методу и это выглядит как капитанство.
khim
24.07.2017 13:41+1Через полгода или просто если его другой человек откроет, уже будет сложно понять и контекст и смысл метода.
Агрумент про полгода, пожалуйста, уберите. Я его слышу уже больше 10 лет, но открываю свой код 10-летней давности… и эффекта не наступает. Да, конечно, мне приходится немного почитать свой собственный код, чтобы «вьехать» в то, что он делает — ну так и комментарии мне пришлось бы читать, какая разница?DistortNeo
24.07.2017 15:16Зависит от опыта программирования.
Первое время (учёба в университете) я открывал код полугодовой свежести и ничего не понимал.
Сейчас уже насобачился писать код, который легко понять даже через несколько лет.vsapronov
24.07.2017 16:51+2Навык вообще важное дело. Но мы же не будем менять отличную практику: не писать всякую текстовую чушь, которая не проверяется компилятором — т.е. комментарии; просто чтобы вам было проще первые полгода и чтобы вы могли почитать тот говнокод, который был произведен в начале этого длинного пути становления программиста?
Пишите говнокод (не важно по каким причинам) — перестаньте как можно скорее. Комментарии вообще не помогут.
domix32
24.07.2017 10:30Иногда люди не очень дружат с английским и использование становится странным и непонятным
vsapronov
24.07.2017 16:54Вот решение этой проблемы вот прямо в самой проблеме — на учить английский. Если такие люди не могут учить английский, значит, скорее всего, код писать тоже не надо.
domix32
24.07.2017 20:42Вы же понимаете, что код надо уже вчера, а английский минимум через неделю? А писать особо и некому, и чувак, по-случаю, вполне неплохо общается с ООП, алгоритмами и прочим, но вот английский дальше "a boy ate an apple" не зашел, но готов доблестно бороться с Google Translate.
vsapronov
25.07.2017 19:50+1Понимаю. Это тяжелая работа — стать хорошим программистом. Знать хоть как-то английский — это всего лишь 10% (если не меньше) успеха. Чтобы его знать, надо прикладывать много усилий. Если человек не готов их прикладывать, например учить его в свободное от работы время, платить за эффективные занятия one-on-one с носителем, и т.д. Прикладывать усилия, понимаете — а не ныть и ждать когда школа/институт/работа научит. Если человек не хочет прикладывать такие усилия, то быть ему быдлокодером… Не потому что не знает английского, а потому что не умеет/не может самостоятельно прикладывать усилия в учебе. А программирование — это сплошная учеба.
За 3-4 месяца интенсива решается вопрос на достаточном для программирования уровне. Так что английский вот вообще не должен стоять как вопрос для программиста — там много других, более сложных вопросов.
kuznetsovin
25.07.2017 09:41Из вашего высказывания получается, что код писать могут только англоговорящие люди. И другим нет места в программировании? По-моему довольно спорное утверждение?
Free_ze
25.07.2017 12:15+2Ну смотрите: и так очевидно, что без английского сейчас сложно устроиться в нормальную контору, нельзя почитать доку на официальном сайте библиотеки и прочее. Но в конкретном примере человек даже не может читать и писать понятный код, то есть делает свою работу плохо.
sasha1024
25.07.2017 15:19+2И другим нет места в программировании?
Им скоро не будет места почти нигде. Пока ещё не так, но к этому всё движется. Мир интегрируется — хорошо это или плохо. Причём в программировании это происходит чуть быстрее ввиду совершенно естественных причин. Не умеешь читать/писать/разговаривать по-английски — считай, не умеешь читать/писать/разговаривать вообще. Конечно, людям с объективными ограничениями (болезнь, возраст и т.п.) стоит сделать скидку, я не спорю. Конечно, теоретически ситуация может поменяться (деглобализация, другой мировой язык или ещё что-то). Но пока выглядит как-то так.
sentyaev
25.07.2017 15:39+1Конечно английский учить не нужно. Пишите на 1С там же русский. Это конечно сарказм.
В современном мире знать свой язык плюс английский это просто самый необходимый минимум для специалиста в любой отрасли.
А в идеали нужно выучить еще хотябы два языка.
vsapronov
25.07.2017 19:57Это не только вопрос знания как такового. Это вопрос адекватности и способности учиться.
Адекватность: если человек в программировании, то он понимает, что большинство языков, технической документации на английском. Адекватность позволяет сделать вывод: хочу стать хорошим программистом — надо учить язык.
Способность учиться: на самом деле не так уж сложно выучить язык на достаточном уровне. Нужно просто приложить усилие и не быть уж совсем дебилом. Теперь, сюрприз-сюрприз, умение учиться — необходимый навык в программировании. Если вы не можете себя заставить выучить язык, то и программист вы, скорее всего, не очень…
sasha1024
25.07.2017 21:59Извиняюсь, подумал и понял, что был неправ. Фактически, мной (и, подозреваю, другими тоже — но не уверен) руководило во время написания этого комментария «всяк кулик своё болото хвалит». То есть у каждого человека есть какой-то свой набор умений (и неумений), и ему хочется верить, что именно его умения — самые важные (а его неумения — некритические). Это из серии «не служил в армии — не мужик», «каждый должен владеть компьютером» и «неумение водить машину в современном обществе равносильно инвалидности».
На самом деле никто не знает, какой именно набор умений наиболее важен (или оптимален при каком-то складе характера). Уж, по крайней мере, не уже-владеющему навыком об этом судить (а вот кого-то, кто бы говорил: «я не умею то-то и из-за этого пострадал там-то» — можно было было послушать). Допускаю, что существуют паттерны жизни, не завязанные на английский, просто я в них не разбираюсь.
MadJackal
23.07.2017 20:39+2Скорее всего, Вы просто сначала думаете, что должен сделать данный программный объект, начерно проектируете его структуру и только потом приступаете к наполнению кодом. К сожалению, правило «Сначала пойми, что ты хочешь написать и только потом начинай кодировать» нынче не сильно в чести…
Xandrmoro
23.07.2017 22:25Как раз если сначала думать и проектировать структуру, то методы, классы и прочие логические единицы выходят самодокументирующимися.
MadJackal
24.07.2017 00:20+1Но на смеси английского языка и языка формальной логики. А так иногда хочется понять, что-же хотел сделать автор — ведь учителя английского у нас, скорее всего были, разные :-)
khim
23.07.2017 21:51+9открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000. И так постоянно. Зачем они так делают?
Не зачем, а «почему».
Мы с одним моим хорошим знакомым по этому поводу регулярно спорим. Для меня программа — это описание решения задачи, но она, в первую очередь, написана на C++ (Java, C#, PHP — нужное подчеркнуть). А комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.
Соответственно нормальное для меня количество комментариев — один-два на пару сотен строк кода и каждый раз когда возникает желание написать комментарий я, скорее, подумаю на тему как отрефакторить код, чтобы он был понятен и без этого.
Для моего же знакомого комментарии — это основное, текст программы — это, так, «что-то такое для компилятора», а описание программы должно быть на «человеческом» языке. И он начинает «кипятком писать», когда в ответ на вопрос «а что сюда, собственно, передавать» он получает ответ «ну из кода же это очевидно!».
Почему среди OpenSource преобладают люди первого вида, а «за деньги» — в основном работают люди второго вида я не знаю…Old_Chroft
23.07.2017 22:12+1комментарии — это налоги «сносок в тексте», поясняющих непонятные или странные моменты, которые никак не удаётся выразить на языке программирования.
Иногда бывает, что красивый, стройный, понятный алгоритм… тупит. Выполняется не приемлемое количество времени. И вот тогда начинается черная магия с рекурсией, вложенными циклами с next/prev, временными переменными, etc. Как вы считаете — надо оставить понятно и медленно, или странно и быстро — но с несколькими строчками комментариев, объясняющих «магию» (и предупреждение «ничего не трогай!» :) )khim
23.07.2017 23:27+4Зависит от того, критичен ли для вас этот код и можете ли вы себе оставить медленную версию.
И да — это как раз такое место где в книге вы можете обнаружить врезку (или сноску) на две страницы, обьясняющую что здесь происходит (возможно с отсылками на другие работы и прочее).
Я не против длинных комментариев, более того — мой рекорд это описание строчек примерно в 50 к фукции из одной строчки (с отсылками на места в разных версиях C и C++ стандартов, обьясняющих почему этот код не просто «случайно здесь работает», а будет работать на всех реализациях, совместимых со стандартом).
Но сам принцип — «комментарий == сноска с пояснением в книге» для меня по прежнему является основным…
TheOleg
24.07.2017 20:03Если код тупит, то скорее всего, это не из-за читаемости кода, а из-за того, что по другому не захотели делать. И от «рефакторинга» добавлением комментариев в духе "// тут рекурсия и вложенный цикл" лучше не станет.
Bonart
25.07.2017 02:05Надо оставить красивый-стройный-понятный и написать рядом оптимизированный.
С комментарием, что второй эквивалентен первому и набором тестов, которые должны проходить оба варианта.
vsapronov
24.07.2017 17:00+1Потому что люди которые пишут «за деньги» не особо интересуются программированием — многие из них, не все, разумеется. Они не живут кодом, не могут понимать язык программирования как человеский, хотя ЯП намного проще по выразительности. Они просто приходят к 9 и уходят в 6. Соответственно и код они производят так себе, который очень трудно понять, ну и лёкий способо это «исправить» — понаписать везде текста, вместо того, чтобы исправить дизайн кода — «уходить же в 6!»…
Free_ze
24.07.2017 17:05+3Они просто приходят к 9 и уходят в 6.
Вы так говорите, будто это что-то плохое.vsapronov
24.07.2017 18:39+2Ну само по себе неплохо, конечно. Это часть собирательного образа известного в русской субкультуре как: «копаем от забора и до обеда». Это когда выработка часов важнее результата. Я больше ссылался на образ, чем на непосредственно на время ухода с работы.
Free_ze
24.07.2017 19:07+1Я все чаще сталкиваюсь с мнением, что быть доступным для работы 24/7 — это признак успеха, причем, что самое милое, у наёмных работников. В то время, когда в подавляющем большинстве компаний творится всякий scrum и все подчинино расписанию, то подобное кивание на часы выглядит странно. Не смог продавить свой чудо-рефакторинг на планировании? Так это ты
ССЗБстахановец, а не те, кто отрабатывает приоритетные таски в заложенные часы.vsapronov
24.07.2017 20:21Ну лично мне удается продавливать рефакторинг, а вообще продавливать приходится довольно редко.
Тут философский вопрос — большинство команд, которые я видел, не парились о дизайне кода вообще никак. Как результат: через годик все покрыто слоемговнакостылей, новые фичи не пилятся вообще: ни по расписанию, ни без расписания, команду разгоняют — поделом. Нужно ли их спасать вопреки их воле.
Я, обычно, рефакторю только те куски, которые нужны мне, если сотрудники или босс явно не просят помощи. Есть большая разница: выправлять свой свежий код, или пытаться потушить пожар из-за говнокода годовалой тухлости. Моя стратегия: чужие косяки идентифицировать и доводить до сведения, не использовать их в своем коде, не фиксить их проактивно, ждать пожара. Когда пожар случится, скорее всего поступает запрос: «пофикси, пожалуйста, проект горит, код говно и т.п.». В ответ надо выдать эстимейт достаточный для хорошего рефакторинга
или полного переписывания говнокода.
Опять же, речь была не о часах по факту. А о менталитете, в котором пребывание на рабочем месте важнее, чем выполненная работа и ее качество. У нас так говорят в конторе: «9 to 6 developers»… Это не значит, что остальные 24/7 вообще.Free_ze
25.07.2017 12:22не фиксить их проактивно, ждать пожара… В ответ надо выдать эстимейт достаточный для хорошего рефакторинга
Хитрая тактика (= Хотя, если вы не зарылись в «пожарах», вокруг все не так уж плохо.
А о менталитете, в котором пребывание на рабочем месте важнее, чем выполненная работа и ее качество.
Мне кажется, что проблема здесь начинается с начальства, которое не волнует качество, а нужно видеть озабоченные лица за компьютерами с 9 до 18. В гос.конторах и других крупных бюрократических организациях такое практикуется.vsapronov
25.07.2017 20:35Хитрая тактика. Я пришел к этому спустя 10 лет спасения утопающих против их воли. Это было всегда больно для всех участников процесса. А потом я понял, что я пытаюсь хакнуть рынок. Рыночные правила работают так: тот кто делает плохой продукт рано или поздно потонет. Некоторые программисты проихводят плохой код — не надо им мешать, пусть рынок вмешается и они пойдут ко дну. Нужно просто оказаться рядом.
С таким подходом сложно не стать мудаком. Потому как есть люди, которые понимают, что что-то не так но не знают как/у кого попросить помощи. Т.е. они как бы и не против улучшаться, но не знают как начать. Ждать когда они потонут — аморально…
Начальство разное бывает. В реальности, у них дихотомия неслабая.
С одной стороны по договору найма (или его аналогу в завсисимости от страны) вам платят за часы работы. Т.е. вы должны работать с 9 до 6 там и даже часто оговаривается как оплачиваются переработки. Чаще же всего резльтат важен вне зависимости от того за какое время он был достигнут. Но начальство не может сказать: сделал работу — иди домой, очень скоро они обнаружат пустые офисы, а постоянное доступное присутствие подчиненных — важный атрибут их власти. Вобщем сложно.Alexeyslav
26.07.2017 08:39Пустые офисы? Врятли… если работа настолько проста… просто подкинет ещё в топку, и так до того уровня на котором работники будут едва справляться и постоянно загружены. Но тут возникнет другая проблема — люди в таких условиях быстро выгорят и перестанут работать вообще.
FreeMind2000
23.07.2017 22:02У ЧакНориса есть такое выражение — «комментарии плохо пахнут».
Т.е. надо писать код так, чтоб комментарии вообще не требовались… Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста. Плюс такого подхода — названия переменных и функций должны быть гипер-продуманными и понятными. Ну и кода без комметариев на мониторе больше видно.
Видимо, в open source кадый мнит себя ЧакНорисом :)ainoneko
24.07.2017 07:37+2Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста.
Если комментарий противоречит коду, то есть проблема, да.
Иногда проблема в том, что код не соответствует замыслу программиста с самого начала.
А если комментария нет, то проблема не видна: код соответствует тому, что он делает, а не тому, что он должен делать.Old_Chroft
24.07.2017 08:32+6Иногда проблема в том, что код не соответствует замыслу программиста с самого начала
Есть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)khim
24.07.2017 13:49Есть такая шутка: код делает то, что написал программист, а не то, что он хотел написать :)
В десятку. Код всегда соотвествует программе. Он может не соответствовать замыслу, он может не соответствовать каким-то великим идеям, но он всегда верно и точно описывает то, что делает программа на самом деле.
А чтобы работать с программой мне, в общем-то, это и нужно. Какое мне, собственно, дело, до тех идей, которые роились в голове у человека, когда он это писал? Да никакого! Мне важно как оно здесь и сейчас работает!alix_ginger
24.07.2017 16:39+1Так речь о том, что код может соответствовать замыслу в одной реализации компилятора, но не соответсвовать в другой
khim
24.07.2017 17:09Ну такой код хоть где-то хоть чему-то но соотвествует. Комментарий вообще может не иметь отношения к тому, что программа делает.
mayorovp
24.07.2017 19:18Да никакого! Мне важно как оно здесь и сейчас работает!
А почему вы по умолчанию считаете, что оно работает? :-)
khim
24.07.2017 19:27Оно всегда работает. Даже если автор породил код путём соединения рандомных кусков со stackoverflow руководствуясь принципом Пусть будет, как будет — ведь как-нибудь да будет! Никогда так не было, чтобы никак не было.
Другое дело, что оно может не делать того, что нам нужно — но почему вы считаете, что человек не способный написать работающий код сможет написать при этом толковый комментарий?michael_vostrikov
24.07.2017 19:55Потому что он знает, что ему нужно, но не возможно не знал (или знал неправильно), как выразить это средствами языка и библиотек.
khim
24.07.2017 20:16Могу поверить что подобное иногда происходит, но в моей практике гораздо чаще бывает так, что код работает (пусть и не совсем так, как его автор предполагал), а комментарий — неверен.
Возможно где-нибудь в аэрокосмической отрасли написание программы дважды (один раз в комментариях, второй — «перевод» на язык программирования) имеет смысл, но в большинстве случае IMNSHO это излишне.michael_vostrikov
24.07.2017 20:30Зачем вы все время говорите про дублирование? Комментарии не должны дублировать реализацию.
// прямое обращение, потому что работает быстрее x->a = b; // хак, чтобы сработал сеттер, так как ... x->a = b;
В зависимости от языка и описания x цели одной строчки могут быть совершенно разные.
michael_vostrikov
24.07.2017 08:29+1Ну обманывает, прям проблема. Заметил, что не соответствует коду — возьми и поправь. При кардинальном изменении логики и комментарии удаляются. А при небольшом в них все равно есть полезная информация. Даже бывает, что становится понятна логика написавшего, и где ошибка. Конечно это если комментарий по делу. Бесполезные комментарии, дублирующие код, не нужны.
khim
24.07.2017 13:53+1Ну обманывает, прям проблема.
Да, таки проблема.
Заметил, что не соответствует коду — возьми и поправь.
Этого невозможно заметить, если вы не читаете код, а читаете только комментарии. А если вы читаете код и понимаете его настолько, что можете исправить и комментарий — то зачем вам там комментарий вообще? Он только к лишней трате времени приведёт.
Бесполезные комментарии, дублирующие код, не нужны.
Тем не менее я видел кучу стайл гайдов, которые требуют обязательно описывать все функции в поноценном doxygen-стиле. В результате имеем кучу комментариев тупо дублирующих код.michael_vostrikov
24.07.2017 14:19+2Этого невозможно заметить, если вы не читаете код, а читаете только комментарии.
А кто сказал, что надо читать только комментарии?
то зачем вам там комментарий вообще?
Затем, что он описывает то, чего в коде нет. "Здесь сделали так, потому что быстрее работает".
обязательно описывать все функции
Это документация, а не комментарии, дублирующие код. Внутренняя реализация может измениться, но если внешнее поведение не изменилось, то и документация не изменится. И наоборот, документация к функции может дополняться без изменения кода. Документация — это не просто комментарии, для нее иногда даже особый формат комментирования делают.
khim
24.07.2017 14:47+1А кто сказал, что надо читать только комментарии?
Никто не сказал. Но обычно люди, жалующихся на острый недостаток комментариев, как выясняется, код читать не хотят вообще. Или, по крайней мере, читают его когда что-то непонятно из комментария. Что, как мне кажется, извращает саму идею довольно сильно: комментарий должен прояснять код, а не код — являться разъяснением спорных моментов в комментарии!
Это документация, а не комментарии, дублирующие код.
В 90% случаев функции делают что-то относительно несложное (что именно — описано в названии, параметры описывают что на входе и что на выходе) и это именно что дублирование даже не кода, а заголовка функции. В случае, когда функция делает какое-то нетривиальное действие комментарии, разумеется, уместны — но таких функций, обычно, немного.michael_vostrikov
24.07.2017 15:34+1Люди обычно жалуются на недостаток комментариев в непонятных местах, а не вообще по коду.
khim
24.07.2017 17:11+2Почитайте топикстартера: Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000.
Жалоба была именно на то, что нет комментариев «вообще», а не на то, что какое-то сложное место не описано.michael_vostrikov
24.07.2017 17:37Бывает, пытаешься понять
Комментарии в таком количестве кода это некоторые точки, в которых можно быть уверенным, что понял правильно, и от них уже отталкиваться.khim
24.07.2017 18:39+2Комментарии этого не помогают понять. Для этого совсем другие документы нужны. Описание архитектуры или что-то подобное — совершенно отдельно от куда и описывающие всё в общих чертах. Иногда в начале кода можно подобное найти в виде отдельного большого комментария — но это не единственный, и зачастую не лучший способ.
Ориентироваться же по комментариям в коде — это всё равно что пытаться понять что есть в книге не по оглавлению, а по сноскам.
Amomum
24.07.2017 12:31+2Самая большая моя боль была при первом столкновении с библиотекой, завязанной на glib. Куча функций вообще без единого коммента. Ладно, внутри функций, бог с ним. Но хотя бы о самой функции — что за аргументы, что она возвращает? Нет, зачем, и так же все понятно.
Т.к. многие функции возвращали указатели, постоянно приходилось угадывать, нужно ли потом память по этим указателям освобождать самостоятельно? Или нужно вызывать g_object_unref? Или можно вообще ничего не делать?
Ох и намаялся я, пока все утечки памяти выискивал.khim
24.07.2017 13:57Т.к. многие функции возвращали указатели, постоянно приходилось угадывать, нужно ли потом память по этим указателям освобождать самостоятельно? Или нужно вызывать g_object_unref? Или можно вообще ничего не делать?
А документацию прочитать — не судьба?Amomum
24.07.2017 14:16+1А как документация для glib поможет мне понять вот эту функцию из библиотеки, которая от glib зависит?
const char *
arv_device_get_string_feature_value (ArvDevice *device, const char *feature)
Она возвращает указатель на строку. Эту строку нужно освобождать? Или не нужно? Комментариев к функции или внутри функции нет вообще. Строка прямо в ней не формируется, она берется из другой функции.
В итоге из-за отсутствия комментария (банально шапки к функции), приходится не просто читать код этой конкретной функции, а прослеживать весь путь, который эта несчастная строка проходит, прежде чем до моего вызова доберется.khim
24.07.2017 14:50+1В таких случаях нужен метакомментарий на модуль (а то и на весть проект), который бы описывал — как это узнать из названия функции. Обычно какой-то стиль является «основным», а функции, которые ему не следуют — помечены особо.
Amomum
24.07.2017 18:22Комментарий для модуля был весьма абстрактный — «Тут описан такой-то класс, он делает вот это». Какого-то единого метакомментария для всего проекта я не обнаружил. Возможно, конечно, я плохо искал…
Судя по git blame комментарии-шапки добавлялись для какого-то генератора биндингов, видимо, некомментированные функции этому генератору были не нужны.
BelBES
24.07.2017 16:51+2Хех… типичный пример API функции у BLAS:
csymm (SIDE, UPLO, M, N, ALPHA, A, LDA, B, LDB, BETA, C, LDC)
Нужно больше боли ;-)
andreysmind
30.07.2017 13:05-1Модные best practices настаивают, что комменты это плохо и все функции\переменные должны быть самодокументируемыми. Поэтому современные каргокультисты от программирования считают коментарии моветоном и максимально их избегают.
dkdkdk
23.07.2017 23:30+2Я Вам более того скажу — молодые программисты на C++ написанием такого кода просто бравируют — "я могу понять как этого будет работать/вычисляться, а моему тимлиду надо пойти освежить свои знания или поломать голову чтобы разобраться".
tandzan
24.07.2017 03:19+1Для себя вывел правило. Если видишь на собеседовании такой код — однозначно попал в молодой и дружный коллектив.
Alexeyslav
24.07.2017 09:11Дело в том что когда программист плотно работает с конкретными терминами и сокращениями для него они становятся очевидными, а длинные имена часто используемых переменных — раздражающе длинными.
nazarpc
24.07.2017 09:52+2Это да, но когда я открываю чужой код, а там месиво из сокращений, понять что происходить очень и очень тяжело. Я бы поставил на то, что автору кода тоже через 3 года читать свой код тоже будет не очень легко.
Ещё бесят сокращения вроде
privKey
(в последнее время работал с кодом для цифровых подписей). Почему не написатьprivateKey
, всё равно ведь IDE дописывает слова сама? Короче, вижу много сомнительных решений и делаю выводы для себя чтобы писать более приятный для чтения код:)MacIn
24.07.2017 12:52+1Когда требуется делать сокращения (например, полное имя идентификатора состоит из слов так 5-6), пишу пояснение в том месте, где происходит объявление. Чтобы человек, читающий код, понимал мою «логику сокращений».
arvitaly
24.07.2017 13:17В чем проблема делать псевдонимы с ссылками на полную версию. Тогда для опытных участников — код всегда будет лаконичным, а для стажеров — понятным.
x893
23.07.2017 20:06Самое хреновое, когда писатели такого кода начинают учить (или заставлять) других как надо писать.
Комментарии конечно вещь полезная, но тут всё зависит от ситуации.
firk
23.07.2017 20:35+6В качестве примера он привел одну успешную книгу популярного автора, который свято верил в то, что чем короче код, тем быстрее он работает.
Кто-нить знает что это за книга? Стало интересно почитать.
alan008
23.07.2017 20:46-2можно научить компилятор анализировать такой код и выдавать предупреждение
На уровне компилятора любая двусмысленность должна порождать либо Warning, либо ошибку компиляции по причине undefined behaviour. Странно, что разработчики компилятора об этом не заботятся, а потом приходится юзать всякие PVS Studio и пр.
Интересно бы услышать мнение Andrey2008SirEdvin
23.07.2017 21:04-3Я думаю, это ближе к особенностям языка. Зачем то же такие выражения как ++i и i++ были нужны…
MadJackal
23.07.2017 21:21-4Обычая перезакладка. Недаром в Go оставили только i++ и не как операцию, а как инструкцию.
nazarpc
24.07.2017 09:54+1С
++i
иi++
легко разобраться если в той строкеi
больше никак не используется. А вот если используется трижды и в пятерых строчках подряд, то мозг начинает закипать. Нужно уметь держать баланс.
grossws
23.07.2017 21:06+12UB — это не ошибка компилятора, а невыполнение программистом контракта.
Например, компилятор быстро выполняет сложение (не проверяет потенциальное переполнение), но программист должен гарантировать, что если у него знаковые числа и переполнение возможно — он его не допустит.
В таком случае компилятор делает сложение с помощью одной инструкции, а не 5, но налагает определенный контракт на разработчика. Если программист не выполнил соответствующий контракт — то поведение не определено (собственно, UB), компилятор имеет право сожрать ваши ботинки.
ivan19631224
23.07.2017 22:17+1> компилятор имеет право сожрать ваши ботинки.
Скорее, компилятор имеет право сгенерить код, который сожрёт ваши ботинки.
alan008
23.07.2017 22:37+1Вот и неплохо было бы Warning получать, чтобы не держать в голове все детали "контракта".
grossws
23.07.2017 22:44+1Будет слишком много ложноположительных срабатываний и warning просто будет после этого отключен.
В том же rust пошли немого другим путём и в debug-сборке, например, overflow при сложении знаковых целых приведёт к панике (исключению), но debug-сборка там обычно работает в разы медленнее. В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си. А кому нужна именно сумма с переполнением напишет
overflowing_add
явно.Krypt
24.07.2017 00:18Вопрос из интереса: а можно ли rust-компилятору сказать, что integer overflow сделан умышленно и ошибки здесь нет?
grossws
24.07.2017 00:50+1https://doc.rust-lang.org/std/primitive.i32.html, см.
checked_add
,saturating_add
,wrapping_add
иoverflowing_add
. Выбираете нужный явно и никто потом не гадает что имелось ввиду и есть ли какие-нибудь особые куски контракта не отраженные в коде.khim
24.07.2017 01:37+2Решение — тривиально до невозможности. В gcc/clang тоже добавили — вот только непонятно почему на это потребовалось чуть не полвека мучений…
grossws
24.07.2017 01:40Оно формально intrinsics?
А мучения, видимо, чтоб жизнь мёдом не казалась. И в стандарт доедут году к двадцатому..
khim
24.07.2017 01:47Оно формально intrinsics?
Угу. Код достаточно оптимальный порождается, семантика тоже определена.
И в стандарт доедут году к двадцатому..
Экий вы оптимист, батенька…grossws
24.07.2017 01:55Экий вы оптимист, батенька…
Я просто работая с поделиями по развитию застрявшими в конце девяностых немного забыл, что уже 17 год)) Так что исправляюсь и скажу, что ждем и надеемся на C37 ,)
Krypt
24.07.2017 08:35-1Надо сказать, что я С++ всеми силами избегаю. Лучше уж сразу на ASM, способов прострелить ногу меньше.
DarkEld3r
01.08.2017 17:08В release-сборке поведение при этом, как минимум, implementation specific, хотя, скорее всего, будет близко к UB из Си
В релизе поведение вполне чётко определено (насколько это может быть для языка у которого нет стандарта).
khim
23.07.2017 23:30Это весьма непросто сделать во время компиляции, так как потенциальное нарушение контракта не является проблемой если программа его реально не вызывает.
michael_vostrikov
23.07.2017 22:58Мне кажется, компилятор или должен генерировать максимально близкий машинный код, передавая ответственность за поведение процессору (и выдавая предупреждение об этом), или не должен его вообще компилировать. А не трогать чужие ботинки.
grossws
23.07.2017 23:19+1Вы готовы пожертвовать 80-90% производительности для десктопа? Получить десятикратное увеличение стоимости услуг и программ? Если да, то есть простые компиляторы с простой кодогенерацией, но в конкурентной гонке general purpose софта побеждают отнюдь не надёжные и медленные программы, которые имеют 5-10% нужного функционала. Пока нет специальных требований на надёжность при допустимой низкой производительности (как в, например, hard realtime), никто этого делать не будет.
При этом есть большая ниша прикладного софта, где нет проблем с производительностью. И пишут её на java/c#/python, где проблемы UB не стоит, т. к. managed окружение с примерно одной виртуальной машиной (на каждую из платформ) за счёт TCK и подобных решений. В случае питона это не совсем так, но близко.
michael_vostrikov
24.07.2017 08:26-1Откуда такие цифры? Хотите сказать, что 80-90% производительности для десктопа достигается за счет кода с UB?) Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.
Free_ze
24.07.2017 12:29+1Я предполагаю, что большинство кода написано без UB
Без, но с учётом же. Таким образом у компилятора есть возможность проводить некоторые свои оптимизации.
khim
24.07.2017 14:04+3Я предполагаю, что большинство кода написано без UB, следовательно, компилятор там ничего убирать не будет.
Вы это серьёзно?
Пример кода, который потенциально может вызвать UB:a = b + c;
. Или ещё так:free(p);
. Или, на худой конец:i++
.
Много вы программ видели, которые ничего этого не содержат? Или где такого кода очень мало? Я видел — это обычно 100500 обёрток, которые и без всякого UB тормозят так, что им никакой компилятор не поможет…michael_vostrikov
24.07.2017 14:25
Тем не менее, компилятор генерирует для этого выражения что-то вродеa = b + c
add eax, ebx
или аналогичного ему по поведению. А не запускает форматирование жесткого диска. Это я и имел в виду под «генерировать максимально близкий машинный код».vbif
24.07.2017 15:29Вы неправы. Он вполне может и не сгенерировать, если посчитает, что так быстрее, а результат будет тот же самый (кроме результатов при UB)
khim
24.07.2017 17:16+1Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению.
Простейший компилятор — да, может быть. Да и отптимизирующий тоже, если это выражение присутствует в отдельной функциии и ничего другого там нет — но кому такая функция нужна?
А вот если «чего другого» там есть, то оптимизирующий можен много чего со всем этим сделать. Может засунуть константу прямо в оператор обращения к памяти, например. Или перенести её куда-то. Да много чего можно сделать если знать, что переполнения не будет. А его не будет, так как программист обещал!michael_vostrikov
24.07.2017 17:43И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c. Даже если a появляется только во внутренних регистрах процессора при вычислении сложной адресации.
khim
24.07.2017 18:48+1И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c.
Не будет вa
суммыb
иc
. Потому что оно туды не влезет. В результате у вас индекс, который, вообще говоря, планировался бытьshort
'ом после оптимизаций окажется равным 70000 — и вы будете материть компилятор на чём свет стоит.
Даже если a появляется только во внутренних регистрах процессора при вычислении сложной адресации.
В том-то и дело, что нет — там появляетсяa
только в том случае если программист позаботился об этом и написал программу так, что она не вызывает UB.
А дальше, если за этим не следить, получается «снежный ком» — тут у нас оказалось 70'000 в short'е, там — мы полезли не в тот обьект, вынули не то, засунули не туда… и вот уже ваша программа самоуничтожается. До форматирования винчестера дело [пока?] не дошло — но всё ещё впереди!michael_vostrikov
24.07.2017 19:07Если после оптимизаций получилась сумма 70000 при входе 2 и 2, значит это неправильные оптимизации. Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000. Потому что это понятное и логичное поведение. Тем более, что компилятор его предупреждал. Поэтому непонятно, откуда у вас получился снежный ком.
В том-то и дело, что нет — там появляется a только в том случае если программист позаботился об этом и написал программу так, что она не вызывает UB.
Вот я и говорю, так быть не должно. Написано, значит должно появляться, или не компилироваться. А не модифицироваться молча с другим поведением в результате модификации.
mayorovp
24.07.2017 19:23+1А где грань? Почему в случае
a = b + c
предполагается доверять программисту что тот ничего не забыл — а в случае, условно,a = b->c;
надо программиста предупреждать?michael_vostrikov
24.07.2017 19:46-2Предполагается, что надо или предупреждать или выдавать ошибку в обоих случаях, в зависимости от заданных настроек компилятора. Ну а в целом, у компилятора есть исходный код, на основе которого можно делать разные оптимизации, даже если оба варианта предполагают сумму двух чисел.
khim
24.07.2017 19:37+2Если же программист сознательно складывает 60000 и 10000 в short, то он не будет материть компилятор, когда не получит 70000.
В том-то и дело, что может получить в результате оптимизаций.
Тем более, что компилятор его предупреждал.
Он его на каждую операцию сложения предупреждал? И программист этого не отключил? Это какой-то неправильный программист и он явно занимается неправильным делом. Ему мандалы из песка делать нужно.
Поэтому непонятно, откуда у вас получился снежный ком.
Снежный ком получается когда у вас одна «невозможность» цепляется за другую «невозможность». Ниже я пример разобрал.
Написано, значит должно появляться, или не компилироваться.
Есть маленькая проблеммка: узнать — вызывает программа UB или нет, в общем случае, невозможно. Проблема остановки, мать её. Потому компилятор исходит из того, что программист — сам себе не враг, UB не допускает и из этого исходит.
О самых вопиющих случаях компиляторы сообщают.michael_vostrikov
24.07.2017 19:54-1В том-то и дело, что может получить в результате оптимизаций.
Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?
Он его на каждую операцию сложения предупреждал?
Я не особо специалист в C++, наверно чего-то не понимаю. То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?
вызывает программа UB или нет, в общем случае, невозможно
Ну компилятор же как-то принимает решение, выбросить этот код так как UB или нет.
vbif
24.07.2017 19:58+1Оптимизирующий компилятор может увидеть после операции сложения операцию вычитания, деления или ещё какую, и поставить её раньше, если решит, что так нужно для оптимизации. А может и не поставить.
khim
24.07.2017 20:22+1Мы же про 16-битный short? Откуда там будет 70000, если для него надо 17 бит?
На многих процессорах нужно предпринимать специальные усилия, чтобы «обрезать» число (x86 — редкое исключение, а не правило) и компилятор, зная о том, что переполнений не бывает вполне может производить вычисления с большей точностью. В x86 так тоже может быть — но в довольно специфических условиях.
То есть, сейчас в программах любая операция сложения это UB, с которой компилятор может сделать все что угодно?
Любая операция сложения потенциально может приводить к UB, если результат «не влезет» в соответствующий тип. Компилятор, в общем, не так часто может понять — будет результат «влазить» или нет. Так что «с консервативным подходом» ему придётся выдавать предупреждения на каждую операцию сложения, про которую он ничего не сможет доказать. То есть про большинство из них.
Salabar
24.07.2017 23:15+2Вы так говорите, словно в компиляторах есть строчка «if (isUndefinedBehavior) system(»format C:\");".
в стандарте языка есть вещи, которые компилятор обязан отслеживать. И они это делают. Соответственно, все оптимизации производятся с учетом этих требований. А неопределенное поведение потому и неопределенное, потому что даже разработчик компилятора не скажет, во что оно выльется.michael_vostrikov
25.07.2017 07:15-1Так разработчик компилятора и не должен ничего говорить. Неопределенное значит неопределенное, пусть процессор и ОС разбираются. Не надо его выбрасывать или заменять на другое, как в примере с циклом. Даже там в комментах шутят про "rm -Rf /". Просто по-моему это не то, что должен позволять стандарт языка.
vbif
25.07.2017 08:43+2Если вам не нравится существование UB — пишите на языках, в которых нет UB. Хотите писать на C — смиритесь. Или напишите компилятор, который вас устроит. Только никому, кроме вас он не будет нужен, потому, что потеряется главное преимущество C — скорость выполнения.
michael_vostrikov
25.07.2017 09:17-3Причем здесь "смиритесь"? Это обсуждение причин и возможностей, а не pull request в стандарт. Поговорить на эту тему теперь тоже нельзя?
vbif
25.07.2017 09:53+2Поговорить-то можно, но проявлять воинствующее невежество не стоит.
michael_vostrikov
25.07.2017 12:13-3Я высказываю аргументы в защиту своей точки зрения. Если я где-то рассуждаю неправильно, укажите где и приведите свои аргументы. Где именно здесь невежество, да еще и воинствующее?
vbif
25.07.2017 12:22+2В неприятии вами того факта, что эффективность оптимизаций, вызывающих UB и не вызывающих может отличаться на порядок.
khim
25.07.2017 13:55Строго говоря ему ещё не удалось привести пример оптимизации, которая не приводила бы к неработоспособности чьй-нибудь программы. Я, впрочем, пример такой оптимизации привести могу. Если у вас в программе написано два раза "
mov eax, ebx
" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на одинmov
. Я не знаю — как это «заметить». Хотя, может быть, и тут можно, просто у меня недостаточно богатое воображение.
Но много вы на таких оптимизациях получите? А просто заменить «лишний»mov
, как было нашим горе-воякой предложено — нельзя.
P.S. Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью. Иначе — никак. Даже в Go и Java — полно UB, потому что манипулировать миром, на который кто-то может смотреть «сбоку» и что-то там изменить так, чтобы этого не стало заметно — практически невозможно.grossws
25.07.2017 15:29Если у вас в программе написано два раза "mov eax, ebx" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov.
При чуть более сложном варианте вида
mov eax, [esi]
два раза уже, по сути, заменить дваmov
а на один нельзя даже в однопоточном коде при запрещенных начисто прерываниях. Вдруг по адресу изesi
лежит mmio-регион.
michael_vostrikov
25.07.2017 15:38-3А просто заменить «лишний» mov, как было нашим горе-воякой предложено — нельзя.
Это был просто пример, вырванный из контекста. Выбрасывание этой инструкции аналогично выбрасыванию любого другого кода. Чем это принципиально отличается от оптимизации в вашем примере?
Строго говоря ему ещё не удалось привести пример оптимизации, которая не приводила бы к неработоспособности чьй-нибудь программы.
Покажите пожалуйста, где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное? Я привел пример с разворачиванием цикла, это вполне нормальная оптимизация. Чем это вас не устраивает?
А в вашем примере оптимизация на основе UB привела к неправильному выполнению программы.
Языки без UB — это языки не дающие использовать несколько потоков и на дающие возможность напрямую манипулировать памятью.
Я не пытаюсь предложить язык, результат компиляции которого будет полностью без UB, и нигде такого не утверждал. Я считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть.
vbif
25.07.2017 15:47Дайте ссылку на коммент, где вы привели пример с разворачиванием цикла? Или вы имеете ввиду «повторить цикл 8 раз, значит надо делать его 8 раз»? Как раз здесь вполне можно оптимизировать вплоть до того, что ничего не повторять ни одного раза, если результат (не считая случаев с UB) от этого не изменится.
michael_vostrikov
25.07.2017 16:20Да. "Можно 8 раз скопировать действия без цикла и выбросить сравнение i".
Как раз здесь вполне можно оптимизировать вплоть до того, что ничего не повторять
В том примере ничего не повторять не получится, так как заполняются значения по указателю. А в целом да, если в цикле одно действие
x = 2;
, то цикл можно убрать, так как результат не изменится. Здесь нет никакого противоречия моим словам. "значит надо делать его 8 раз" было сказано в контексте того примера, так как там поведение цикла распространяется за пределы цикла и функции.
khim
25.07.2017 16:41+2Чем это принципиально отличается от оптимизации в вашем примере?
Ничем — и в этом-то всё и дело.
Покажите пожалуйста, где я утверждал, что оптимизация должна гарантировать работу стороннего кода, считающего такты или что-то подобное?
Дык эта:Я считаю, что компилятор наоборот не должен полагаться на UB и должен оставлять его как есть.
Вот прямо-таки туточки.
Вы либо трусы наденьте, либо крестик снимите. Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB («считает такты или что-то подобное»), либо нет.
Потому что пока ваши хотелки выглядят так: копилятор имеет право делать для оптимизации что угодно — но не должен ломать моих программ… чужие — можно.
Так не бывает, уж извините. Нужен полный и точный перечень того, чего «хорошая», «правильная» программа делать не должна (в частности, по вашему — не должна считать такты). И вот этот список — и есть список UB, на которые полагается ваш компилятор.
Компилятор, который оставляет все UB как есть — не может оптимизировать, фактически, ничего и никак…michael_vostrikov
25.07.2017 17:12-1Ничем — и в этом-то всё и дело.
Почему вы тогда сказали, что "просто заменить нельзя", если компиляторы проводят такие оптимизации? Раз ничем не отличается, значит можно.
Вот прямо-таки туточки.
Не вижу связи. Не могли бы вы прямо по пунктам написать логические выводы, приводящие к противоречию? Мне правда интересно, возможно я что-то не так понимаю.
Оптимизации бывают не только из-за UB. И не каждую ситуацию из тех, которые в стандарте C++ называются UB, нельзя оптимизировать. Я ниже привел примеры под спойлером, что я имею в виду под "не должен полагаться на UB".
чужие — можно
в частности, по вашему — не должна считать тактыНет, это не по-моему, это вы сами додумали. Я наоборот говорю, что не должно быть требований, что программа что-то должна. Считает — ее дело, пусть программист учитывает, что может быть оптимизация.
Либо у вас оптимизатор имеет право поломать программу, которая вызывает UB, либо нет.
Оптимизатор должен обеспечивать то же поведение. Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же. А не
a = 42;
не может оптимизировать, фактически, ничего и никак…
Я же привел примеры. Развернуть цикл можно? Можно.
khim
25.07.2017 18:04+2Оптимизации бывают не только из-за 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. Только не надо про то, что «стало только лучше» и «скорее всего программист этого и хотел». Эти критерии — неформализуемы и их, в общем, невозможно использовать для того, чтобы решать — является ли что-то хорошей оптимизацией или нет.
vbif
25.07.2017 18:09+3Оптимизации бывают не только из-за UB
Не из-за а благодаря.
Если до оптимизации было 2 граничных случая, то и после нее должно быть столько же.
А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.
Развернуть цикл можно? Можно.
Какая разница, сегодня или месяц назад, главное собаку-то я покормил!
P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.khim
25.07.2017 18:20+1P.S. А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
О невозможности UB. Невозможности, которую должен обеспечить программист.
Любая оптимизация опирается на тот факт, что программу у нас «хорошая», чего-то «плохого» не делает. Вот если это «плохое» в принципе «железо» позволяет сделать — это и есть UB.
Понятно, что тут всегда встаёт вопрос компромиса: что именно мы можем заставить разработчика не делать, а чего не можем. То есть вопрос отнесения чего-то к UB — это вопрос обсуждаемый. Что-то, что стандарт называет UB, конкретный компилятор может и не называть UB, а, наоборот, допускать что в программе такое происходит. А может иметь свои UB (обычно считается, что это «дефект, который когда-нибудь пофиксят», но это когда-нибудь может растянуться на долгие годы). Но если уже что-то отнесено к UB — то, разумеется его в программе быть не должно и компилятор вправе на это полагаться.
Выдача же при этом полезных предупреждений — отдельная и весьма сложная задача.
michael_vostrikov
25.07.2017 19:47-3А откуда известно, какие «2 граничных случая» были до оптимизации? Учтите, что C — кроссплатформенный язык, и сколько там «граничных случаев» будет на той или иной платформе — неизвестно.
Из исходного кода. Если в исходном коде присваивание было только при условии, то после оптимизации не должно присваиваться всегда. Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет. PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.
А цикл развернуть без предположений о возможности UB тоже нельзя, и вам уже несколько раз показали почему.
Вы упорно не хотите понять, что я пытаюсь объяснить. Случаи, которые можно назвать UB, бывают разные. Если коротко, то что делает PVS-Studio, должен делать компилятор.
khim
25.07.2017 20:09+1Тем более что в правильно написанном коде переменная должна быть инициализирована, и такой оптимизации не будет.
Но если человек её не проинициализировал, то это значит что ему всё равно что оттуда вернётся — так почему бы и не 42? Кода меньше, а в правильно написанной программе — это «стрелять» не должно.
PVS-Studio ведь как-то находит такие ошибки, несмотря на то, что C — кроссплатформенный язык.
PVS-Studio — это отдельный продукт, специально «заточенный» под то, чтобы отлавливать ошибки в программах. А компилятор — это компилятор. Компилятор решает одну задачу: сделать так, чтобы программа не вызывающая при своём исполнении UB работала быстро и требовала мало памяти. Всё остальное — не к компилятору. Не надо превращать компилятор в Die Eierlegende Wollmilchsau, пожалуйста, это приведёт только к тому, что все задачи будут решаться одинаково плохо.
vbif
25.07.2017 23:07Тот же «исходный код» может быть библиотекой, которая используется в 100500 разных проектах. И та или иная часть её может никогда не понадобиться. И без соответствующих оптимизаций придётся либо при каждом вызове проверять кучу условий, которые всегда не выполняются, крутить ненужные циклы и пожертвовать 90% скорости на ненужный мусор. Либо нагородить 100500 вызовов, делающих одно и то же, но чуть по-разному.
michael_vostrikov
25.07.2017 15:37-2С этим никто и не спорит. Можно просто взять и выбросить половину программы, работать будет быстрее, только не так как ожидалось. Я говорил, что компилятор не должен делать это молча. А спорю я потому что мне говорят, что это правильно и по-другому никак. И при этом нет особых доказательств кроме «в стандарте так, смиритесь».
Кстати, вы можете привести пример таких оптимизаций? Нашел в гугле пару ссылок, попробую объяснить, что я имею в виду.
Скрытый текст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
, это будет известно на этапе компиляции, и можно показать предупреждение.vbif
25.07.2017 15:59+1UB называют любые ситуации, когда результат выполнения инструкции не регламентирован в стандарте.
michael_vostrikov
25.07.2017 16:24-1Это я понял. Я имел в виду, что они все-таки различаются, и работать со всеми одинаково нельзя, потому что это приводит к проблемам.
vbif
25.07.2017 16:38работать со всеми одинаково нельзя
Что значит «одинаково работать»?michael_vostrikov
25.07.2017 17:19Менять в любом из этих случаев код на любой другой, нужный для оптимизации, предполагая, что поведение будет такое же.
khim
25.07.2017 17:24Менять в любом из этих случаев код на любой другой, нужный для оптимизации, предполагая, что поведение будет такое же.
Извините, но кто будет решать — когда код менять можно, а когда — нет? И как?michael_vostrikov
25.07.2017 19:27Решать будет компилятор. Только надо разделить понятия "UB из-за неинициализированной переменной" и "UB так как возможно кто-то 8 байт пишет из другого потока". В первом случае это написано в коде, во втором нет.
khim
25.07.2017 20:19+1Только надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока».
Они уже разделены. Я боюсь вы смешиваете два понятия: поведение, определяемое реализацией (что-то, что разные компиляторы могут делать по разному) и неопределённое поведение (то, чего в программе случаться не должно и то, чего программист не должен делать никогда).
В первом случае — компилятор обязан обеспечить некоторое разумное поведение (скажем если вы засунете 2147483648 в 32-битныйint
, то получите либо -2147483648, если у вас используется дополнительный код, либо -0, если у вас используется прямой код, но ничего «странного» при этом произойти не может), во втором — компилятор может делать всё, что угодно. Совсем что угодно.
Зачем вам потребовалось ещё как-то этот волос расщеплять и кому от этого станет легче — мне неведомо.
В первом случае это написано в коде, во втором нет.
Что значит «написано» и «не написано»? 8 байт кто пишет? И как? Пушкин? С того света, что ли? Конечно же в программе есть где-то код, где и эти 8 файт атомарно пишутся…
khim
25.07.2017 17:21+1Такая оптимизация маскирует ошибки, хотя задача компилятора эти ошибки находить.
Задача компилятора — компилировать. Для нахождения ошибок — есть другие инструменты. То, что компилятор, по совместительству, является ещё и статическим анализитором и линтером — это хорошо, но это не является его основной работой!
Так имеет право компилятор сделать такую оптимизацию или нет?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 и должен оставлять его как есть). Одновременно.
grossws
24.07.2017 18:03Давайте возьмем гугл и посмотрим на производительность 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 программистом) вы получите работу инкремента за одну инструкцию.
michael_vostrikov
24.07.2017 18:39-1Без этого банальный инкремент int'а будет занимать не одну инструкцию, а 3-4 с условным переходом
Я говорю про абсолютно обратную ситуацию. Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении. Или вообще не компилируем. Но не додумываем за программиста и не выкидываем просто так.
более топорная кодогенерация
Это не то, что я имел в виду.
grossws
24.07.2017 18:56+1Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении.
После какого количества warning'ов вы их отключите? После первых нескольких тысяч? Или раньше?
Есть другой подход, с контрактами и доказательствами. Но это будет очень сложно и дорого в разработке. И, опять же, программист должен будет доказывать, что переполнения не случится.
michael_vostrikov
24.07.2017 19:12-2Я поменяю или тип или размер результата. А если не поменял, то пусть оно случается. Если вообще случится. Выкидывать такой код вообще или модифицировать его с другим поведением это неправильно.
khim
24.07.2017 19:04+2Написано сложить две переменных в памяти и поместить в третью — генерируем инструкции для сложения и присваивания, выдаем предупреждение о возможном переполнении. Или вообще не компилируем.
Это примерно то, что делает tcc. Потери производительности, как вам уже сказали — от 2х раз до 10. Хотя он не вполне неоптимизирующий.
Но не додумываем за программиста и не выкидываем просто так.
Для этого нужно чётко описать что такое «не додумываем до программиста». И как это может соотноситься с вот этим:
А что вы имели в виду?более топорная кодогенерация
Это не то, что я имел в виду.
В примере, который я уже приводил компилятор всего-навсего заметил, что у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла (а они, как бы, время занимают, да) — и использовать для выхода из цикла эту переменную. Законно так сделать? В отсутствие переполнений — да, разумеется. Простая линейная замена. Весьма и весьма полезная в разных матричных алгоритмах.
А дальше — другой код, про UB ничего не знающий проверил когда в «усовершенствованной» программе происходит выход из цикла. И выяснилось что для этого число должно стать больше максимального числа, который может поместиться вint
. То есть у нас там — проверка, которая всегда ложна. То есть выход из цикла не случится никогда. Законная такая замена? Разумеется — опять-таки в случае отсуствия UB.
А в результате — цикл вместо заполнения таблички уничтожает всю вашу программу на корню.
В том-то и дело, что вы либо «додумываете за программиста и выкидываете просто так» куски кода — тибо у вас «более топорная кодогенерация». А как иначе? Как, по вашему, компилятор должен что-то оптимизировать, если он ничего никуда не может подвинуть и ничего ни откуда не может удалить?michael_vostrikov
24.07.2017 19:28-3у нас кроме счётчика цикла есть ещё и другая переменная, к которой прибавляется в цикле постоянно одно и то же число. И, стало быть, можно выкинуть все манипуляции со счётчиком цикла
Неправильно. Во-первых, переполнения существуют, и компилятор должен об этом знать. Во-вторых, написано, повторить цикл 8 раз, значит надо делать его 8 раз. Написано, выйти из цикла, когда переменная i станет 7, значит надо сравнивать i и 7. Можно 8 раз скопировать действия без цикла и выбросить сравнение i. Но не заменять одну проверку другой. Это именно то, о чем я говорю — компилятор додумал за программиста. А дальше да, снежный ком.
Как, по вашему, компилятор должен что-то оптимизировать
Пример:
mov eax, ebx
mov ebx, eax
Заменяется одной инструкцией:
mov eax, ebx
Результат со всеми изменениями в состоянии регистров тот же самый.
Оптимизация? Оптимизация.
vbif
24.07.2017 19:33+2Далеко на таких оптимизациях не уедешь. К тому же даже такая оптимизация имеет допущения, что никто не должен читать регистры между выполнением инструкций, что вы не считаете такты процессора. Пусть обратное кажется невероятным.
khim
24.07.2017 19:46К тому же даже такая оптимизация имеет допущения, что никто не должен читать регистры между выполнением инструкций, что вы не считаете такты процессора.
Ну считать такты в современных процессорах никто не будет, а менять регистры — таки да. Я с таким лично сталкивался в JIT-компиляторе. Но это редкость. Но если учесть ещё, что C, в общем-то, обычно не все переменные в регистрах (более того, в C++20 их вообще нельзя будет на регистры класть), то окажется, что вам вообще ничего трогать нигде нельзя будет — ибо sigaction/kill вдруг работать перестанут (ну или многопоточные программы вспомните, если у вас нет в OS работающих sigaction/kill).vbif
24.07.2017 19:51Смотря в каких процессорах опять же, наверняка ещё есть куча микроконтроллеров, в которых гигагерцы тактовой частоты и таймеры — непозволительная роскошь. Хотя с другой стороны, если считать такты, писать надо сразу на ассемблере.
michael_vostrikov
24.07.2017 19:58Это применимо к любой оптимизации.
khim
25.07.2017 02:17+1Правильно — потому требование «не додумываем за программиста и не выкидываем просто так» запрещает все и всяческие оптимизации.
В том-то и дело, что любая оптимизация опирается на то, что код определённых вещей не делает и, соответственно, «не заметит» разницы между оптимизированной и не оптимизированной версией.
Правильный код на C/C++ не считает такты процессора, не меняет регистров из обработчика сигналов, не обращается к переменным, которые были освобождены c помощьюfree
и к переменным вообще из разных потоков без синхронизации (за исключением строго описанных случаев — см.volatile
)… в общем код — не вызывает UB никогда и нигде.
Вот тогда его можно как-то оптимизировать. Иначе — никак.michael_vostrikov
25.07.2017 07:21-3Я же написал, как можно сделать оптимизацию в приведенном вами примере без изменения ожидаемого поведения. "Ожидаемое" — это то, что написано в исходном коде. Значит иначе тоже можно.
khim
25.07.2017 13:44+2Я же написал, как можно сделать оптимизацию в приведенном вами примере без изменения ожидаемого поведения.
Вы написали чушь собачью. Ожидаемого кем? Ожидаемого когда?
«Ожидаемое» — это то, что написано в исходном коде.
Нельзя. Для этого нужно «знать» что происходит во всей программе.
Рассмотрим ваш пример и немного его расширим.
Неоптимизированный (работающий) код:
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. Если второе — то всё ещё остаётся вопрос какие оптимизации являются хорошими.michael_vostrikov
25.07.2017 19:43-3Ожидаемого кем? Ожидаемого когда?
Ожидаемого от программы кем-то, кому нужен ее результат. Ожидаемого в результате компиляции кода. Ожидаемого во время его работы. Что за философские вопросы?
Как, почему, зачем, за что? А очень просто: я беру sigaction/pthread_kill и по сигналу из другого потока обнуляю ebx
И? Что вы после этого будете говорить?А причем здесь другой поток? Компилятор про него ничего не знает, у него есть только исходный код. Я уже несколько раз это говорил. Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.
Ваша «безопасная и надёжная» оптимизация превратила работающую программу — в неработающую
И снова я напомню, что это был не рецепт на все случаи жизни, а абстрактный пример, оторванный от контекста конкретной программы с потоками или без и приведенный в контексте разговора про изменения машинного кода. И там и там есть изменения, но одни ломают однопоточную программу, а другие нет.
Вы написали чушь собачью.
Хороший аргумент. Пожалуй, на этом стоит закончить.
khim
25.07.2017 20:53+1Ожидаемого от программы кем-то, кому нужен ее результат. Ожидаемого в результате компиляции кода. Ожидаемого во время его работы.
То есть в компилятор должен быть встроен ещё и модуль телепатии, который будет читать в голове у автора его ожидания? Я боюсь вы переоцениваете способности создателей компилятора, однако.
Что за философские вопросы?
Это не «философские вопросы». Это набор «правил игры». Если конструкция имеет некоторое определённое поведение, то, стало быть, есть некоторые ожидания от того, что будет делать программа в которой такая конструкция встретилась. Если же поведение не определено — то никаких ожиданий нет. Любое поведение — будет ожидаемым. Без каких-либо ограничений.
В этом сама суть неопределённого поведения. Оно потому и не «неопределённое», что никаких ожиданый у вас по поводу такой программы быть не может.
Компилятор про него ничего не знает,
Как это не знает?у него есть только исходный код.
И в этом коде — есть функция, которая, из другого потока, меняетebx
. Так что знает он всё, конечно — вопрос только в том, что использовать это знание, как бы, проблематично.
Насколько я знаю, чтобы ему это сообщить есть ключевые слова типа volatile.
Совершенно верно. Естьvolatile
, есть библиотека атомарных операций и вообще есть много разного-всякого, что позволяет вам проделывать подобные штуки не нарываясь на UB. И вот только и только тогда, когда ваша программа написана так, что она не вызывает UB — вы можете иметь какие-то «ожидания».
И снова я напомню, что это был не рецепт на все случаи жизни, а абстрактный пример, оторванный от контекста конкретной программы с потоками или без и приведенный в контексте разговора про изменения машинного кода.
Оп-па. То есть мы, оказывается даже подобную оптимизацию можем проделать не всегда, а только тогда, когда нам кто-то разрешит.
Вам юристом бы стоило пойти работать. Это у них всё всегда зависит от контекста и нет никаких «жёстких» правил игры, случае буквально до буквы совадающие могут быть трактованы по разному из-за каких-то совершенно мелких тривиальностей…
А у разработчиков компилятора — всё проще. Если одно выражение (обычно более простое), есть другое (обычно более сложно), последнее — можно заменить на первое если на программах не вызывающих UB результат не изменится. Всё.
Что там и как будет у программ, вызывающих UB — их не волнует от слова «совсем».
Пожалуй, на этом стоит закончить
Ну дык. Вы уже сдали половину своей позиции и перешли от "Написано, значит должно появляться, или не компилироваться" к "Надо разделить понятия «UB из-за неинициализированной переменной» и «UB так как возможно кто-то 8 байт пишет из другого потока»". Если мы не остановимся, то ещё через день-другой до вас, возможно, наконец дойдёт, что и разделать эти понятия, в общем, не нужно — нужно просто по другому по другому рассклассифицировать некоторые случае в стандарте и перенести их из категории неопределённого поведения в категорию неуточняемого поведения. С чем, как бы, особо никто и не спорит. Вспомните мою реплику: Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…michael_vostrikov
25.07.2017 22:15-1То есть в компилятор должен быть встроен ещё и модуль телепатии, который будет читать в голове у автора его ожидания?
Нет. У компилятора есть исходный код.
и перешли от "Написано, значит должно появляться, или не компилироваться"
Нет. Эта фраза относилась к случаям вида "UB из-за неинициализированной переменной" и конкретно к примеру, где компилятор заменил одну проверку на другую.
Впрочем ладно. Я попытался объяснить, но у меня не получилось. Будем считать, что я не прав.
vbif
25.07.2017 23:14+3Программисты часто через некоторое сами не могут понять, что они хотели, когда писали этот код, вы думаете, что компилятору это под силу?
akzhan
24.07.2017 10:30-1в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).
в C# есть implementation-specific behavior, плюс unsafe.
И как уже заметили, производительность практически никак не связана с неопределенным поведением.
Free_ze
24.07.2017 12:49+3И как уже заметили, производительность практически никак не связана с неопределенным поведением.
Допустим — проверка границ массива. В C/C++ — это UB, Java/C# лепят рантаймовые проверки. Вполне ощутимый оверхед, зато позволяет точно сказать, что будет, если «промахнуться». Всякие точки следования, порядок вычисления операндов — аналогично позволяют оптимизировать вычисления в ущерб некоторым гарантиям.
khim
24.07.2017 14:07+3в Java определено поведение при вычислении выражений (неужели вы думаете, что этим только и ограничивается неопределенное поведение?).
Не только этим. В языках без GC избавиться от UB черезвычайно сложно. А добавить в C++ GC или borrow checker — это уже совсем-совсем другой язык получится.
Другое дело, что можно было бы 3/4 (а то и больше) UB превратить в implementation-specific behavior без большого вреда для скорости — но уж как сделано так сделано…DistortNeo
24.07.2017 15:44В языках без GC избавиться от UB черезвычайно сложно.
Я бы сказал, что GC здесь идёт параллельно.
А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.khim
24.07.2017 17:20+1А основная причина UB здесь — предоставление языком программирования прямого доступа к памяти.
При ручном выделинии и исвобождении памяти UB неизбежен, так как нет возможности убедиться, что в моментfree
«живых» указателей на этот участок памяти больше нет. А если можно — то зачем этотfree
, собственно, нужен?
Скажем в Ada (суровый такой аэрокосмический язык) прямого доступа в память нет, а UB — таки да. Именно из-за отсуствия GC. Ada с GC — UB уже не имеет, там всё жёстко определено.mayorovp
24.07.2017 19:29Не совсем так. Если напутать с освобождением памяти — будет повреждение кучи. Ситуация неприятная — но все же не настолько как UB.
Повреждение памяти роняет программу после того, как произойдет. Его можно отловить в отладчике, если постараться.
UB же способно испортить программу еще до того как произойдет — и такие баги ловятся только плясками с бубном или изучением ассемблерных листингов.
khim
24.07.2017 19:53+1Ситуация неприятная — но все же не настолько как UB.
Вообще-то обращение к обьектам, которые были удалены — один из вариантов UB. Так что неясно что эта фраза вообще должна значить.
UB же способно испортить программу еще до того как произойдет — и такие баги ловятся только плясками с бубном или изучением ассемблерных листингов.
Нет, нет и нет. 100500 раз нет. Программа может содержать сколько угодно потенциальных UB, однако если при её конкретном запуске никакого UB не случилось — то она обязана работать.
Иначе вообще ничего нельзя было бы на C/C++ написать в принципе, ибо, как уже говорилось любоеi++
— это, потенциально, UB.
Если вы найшли UB, который что-то испортил до того, как произошёл (так бывает, хотя и очень редко) — это повод «трубить во все колокола», файлить баги на компилятор и т.д. и т.п. Этого быть не должно, точка.mayorovp
24.07.2017 20:02Вот вам пример:
if (a) printf("a is not null\n"); b = a->c;
Если оптимизатор докажет, что переменная a не может измениться в результате printf — он может выкинуть проверку. В итоге, когда a все-таки окажется нулевым указателем, получится что UB "проявилось" до того как произошло — что значительно затруднит отладку.
Или я не прав?
khim
24.07.2017 20:37+1Нет, так компилятор делать не может. Вот если переставить строки местами — тогда да, тогда компилятор может выкинуть проверку, а после этого, подняпрягшись, может выкинуть и
b
тоже, в результате чего, скажем, ваша программа перестанет правильно реагировать наnullptr
(я приводил законченный пример в другом комментарии).
Однако всё это происходит после точки, где вы вызвали UB! То есть да, UB может приводить не только к тому, что ваша программа станет делать что-то плохое, но и к тому, что она перестанет делать что-то плохое… но всё это происходит после того, как она, условно говоря, «пересекла» точку невозврата.
P.S. Есть правда, исключение: компилятор может модицицировать код, который не вызывает побочных эффектов и тогда, в некотором смысле, UB будет распространяться «назад по коду» — но это и без всяких UB может происходить.
Andrey2008
23.07.2017 23:29+4А что тут комментрировать… Да, язык ужасно сложный и коварный. Но уж какой есть. Приходится использовать вспомогательные инструменты, такие как PVS-Studio.
Сам то я только рад такому положению дел. Это приносит нам деньги. :)
Выдавать предупреждения надо очень аккуратно. И многие предупреждения мы не делаем именно по той причине, что будет слишком много ложных срабатываний (см. Философия статического анализатора PVS-Studio). А когда делаем какие-то диагностики, то стараемся учесть эмпирические нюансы, которые подсказывают есть ошибка или нет.
Old_Chroft
23.07.2017 21:40+11Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте
Фразе больше 20 лет. С неё и надо начинать учить программирования. А за code-ниндзюцу, приведенное в статье, надо прищемлять пальцы дверью, что бы больше такого написать не смог уже никогда. Ну, если это не «патч Бармина» и тому подобные вещи — но они и сделаны для того, чтобы их сложно было прочитать.Temtaime
24.07.2017 00:54-5Если мой ЯП строго определяет порядок вычислений и всё описанное в статье чётко определено — почему это означает то, что я пишу говнокод?
lastrix
24.07.2017 03:40+6- Не все знают стандарты наизусть. Более того, однажды выучив, через какое-то время удивишься, что часть уже забыл или исказил;
- Нужно думать гораздо больше, чем ожидается от среднего по больнице;
- Отладчики работают почти всегда построчно, а если и есть возможность по операторам, то сделать это проблематично в плане интерфейса — неудобно;
- Порядок выполнения задает номер строки или скобочки, но никак не стандарты — вот правило, которое должен знать каждый программист.
sasha1024
24.07.2017 13:21Порядок выполнения задает номер строки или скобочки, но никак не стандарты.
Я не распарсил.khim
24.07.2017 14:09Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу и, разуеется, не нужно менять в одном и том же выражении одну и ту же переменную дважды.
sasha1024
24.07.2017 14:18Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.
Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.
Но, например, в Паскале порядок операций достаточно простой — и нету писать «1+(2*3)», «чтобы было понятней».
Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (аля сложения и умножения) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
sasha1024
24.07.2017 14:24Имеется в виду, что не нужно заставлять читателя вспоминать вот эту безумную таблицу.
Ога, да, с этим совершенно согласнен, в Си-пододных языках эта таблица нелогичная и запомнить её сложно и не нужно.
Но, например, в Паскале порядок операций достаточно простой (а в третьем языке ещё логичнее) — и нету смысла писать «a + (b * c)» лишь «чтобы было понятней».
Порядок выполнения операций «задают не стандарты» именно в Си; в языках с более осмысленным порядком операций как раз вполне могут задавать стандарты; и даже в Си некоторые наиболее простые операции (ал-я сложение и умножение) мы позволяем компилятору упорядочить по стандартам (а не ставя везде скобочки).
nApoBo3
24.07.2017 10:46+1Стандарты могут меняться.
Ваш код возможно будет поддерживать или читать человек не знакомый с деталями стандартов.
Ваш код возможно будут портировать на языки имеющие иную особенность реализации подобных выражений.
Разные разработчики по разному трактуют использование подобных выражение. Упрошенное выражение сможет прочитать любой разработчик, сложное выражение только регулярно его использующий, остальные полезут в доки.
vbif
24.07.2017 19:26Порядок выполнения задает номер строки
Лучше без нужды на это правило не полагаться.
s-kozlov
28.07.2017 19:55-1С неё и надо начинать учить программирования.
Ничего не выйдет: студенты банально не поймут, что такое «сопровождать код». Потому что сопровождение кода — это разновидность секса, а ему, как известно, можно научиться только на практике.
geher
24.07.2017 09:20+1Порядок вычисления выражений...
Некоторые языки программирования (например, Cache ObjectScript) приучили меня расстаалять скобки даже там, где они не нужны.
Вроде такого: 2+(2*2)
AllexIn
24.07.2017 10:37Теперь, допустим, что вы знаете (и эти знания компилятору недоступны), что метод подсчета налога calculate_tax обновляет значение локальной переменной налог (tax) в объекте, но не изменяет базовую цену; итак, для вас это предупреждение будет ложной тревогой.
Вот только правила проектирования говорят нам, что мы не должны писать код на основе знаний о внутреннем поведении. Ибо поведение изменится и всё сломается.sasha1024
24.07.2017 13:27В теории — да.
На практике возможности задания сигнатур функций в большинстве современных языков программирования достаточно убоги (например, мы не можем прямо в сигнатуре функции сказать «вот эти поля объекта this она имеет право перезаписывать, эти — не имеет, а эти — даже читать не имеет права»). Остаётся писать такое в документации; или полагаться на здравый смысл, если документации нету.
Хотя, конечно, можно взять за правило считать «на всё, что не написано прямо в объявлении функции или в официальной документации, полагаться нельзя» (но и с этим может быть сложно).
erwins22
24.07.2017 12:05А если спросят на собеседовании?
Сказать, что разработчики С# тоже не знают ответа на этот вопрос?mayorovp
24.07.2017 12:50+3Сказать, что вы такого кода писать не будете. А если встретите чужой — то поведение всегда можно увидеть в отладчике.
erwins22
24.07.2017 14:53-5Т.е. сознаетесь в некомпитентности?
У меня знакомый когда устраивался С++ разработчиком после 3-4 собеседований учил нечто вроде
for(int = i;i-- < 6;++i++)
DistortNeo
24.07.2017 15:59+4Я бы не стал работать в таком месте. Только идиот будет писать такой код.
А здесь будетerror: lvalue required as increment operand
AllexIn
24.07.2017 16:00+1Т.е. сознаетесь в некомпитентности?
Всё знать невозможно и не нужно.
Фундаментальные алгоритмы знать — это одно. А уметь писать/читать скрижали — другое.
Free_ze
24.07.2017 16:16+2Т.е. сознаетесь в некомпитентности?
Компетенция программиста — навык писать поддерживаемый код. Учить это смысла нет вообще никакого, хотя для C++ стоило бы поинтересоваться, почему так писать неправильно.
grieverrr
24.07.2017 15:05-4А можно тупой вопрос? Вот этот вот оператор, ++, он вообще в нынешних реалиях, зачем нужен? Ведь по сути это такая примитивная лямбда.
sasha1024
24.07.2017 15:48Моё мнение.
- Обязательно должен быть оператор взятия следующего/предыдущего элемента. Не меняющий значение операнда, а просто возвращающий следующий/предыдущий элемент. То есть не каждый тип данных будет поддерживать операторы «a + целочисленное_значение» и «a — целочисленное_значение», но операторы следующего и предыдущего элемента будут поддерживаться бо?льшим числом типов (но если тип поддерживает «a + целочисленное_значение»/«a — целочисленное_значение», то на программиста должен накладываться контракт, что «a + 1»/«a — 1» делают то же, что и операторы следующего/предыдущего элемента).
- Опционально могут быть предусмотрены операторы, сокращающие записи вида «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 имеет второй, но проваливает первый.
Паскаль имеет первый, но не имеет второй.Dima_Sharihin
25.07.2017 08:00То есть вы предлагаете в Си запихать итераторы из C++ STL?
sasha1024
25.07.2017 08:11Ума не приложу, откуда Вы это взяли.
Если Вы про то, что в Си++, начиная с версии 11, появились функции std::next и std::prev — то это всего лишь методы (а не операторы).Dima_Sharihin
25.07.2017 09:03Не очень понимаю просто, зачем это вводить на уровне синтаксиса языка.
функции std::next и std::prev
Да зачем, ::iterator + 1 даст следующий элемент контейнера, будь он вектором, словарем или еще чем-то.
sasha1024
25.07.2017 09:18Не даст, например, для списка.
Но даже не в том дело. Просто, очевидно, система неполная, некрасивая и кривая. Рано или поздно это аукнется.
YoungSkipper
24.07.2017 17:52-1Этот код вызовет предупреждение: компилятор увидит, что метод calculate_tax не является константным (const), поэтому он обеспокоится тем, что метод может изменить переменную base_price — и в этом случае иметь значение будет то, считаете ли вы налог по оригинальной base_price базовой цене, или по уже измененной.
Этот код вызовет предупреждение (если вызовет) в не зависимости от того помечен ли метод calculate_tax константным или нет. Компилятору вообще практически всегда наплевать на ваши const определения.
andreysmind
30.07.2017 13:11-1«Чтобы все офигели как я могу».
В моем рабочем опыте это обычно показатель законченного эгоиста, который не заботится о том, как код будут читать другие программисты.
malinichev
Жуть конечно, но это в порядке вещей… Я когда заказывал сайт на фрилансе такое тоже встречал)