Вашему вниманию предлагается перевод статьи Рэймонда Чена из блога The Old New Thing, посвященной проблемам кода, полагающегося на порядок вычисления выражений — и всем тем, кто пишет foo(i++, a[i]);
Порядок вычисления выражений определяется конкретной реализацией, за исключением случаев, когда язык гарантирует определенный порядок вычислений. Если же в дополнение к результату вычисление выражения вызывает изменения в среде выполнения, то говорят, что данное выражение имеет побочные эффекты.
MSDN
В нашей внутренней рассылке про C# регулярно возникает дискуссионный вопрос, который касается корректной интерпретации подобных конструкций:

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)


  1. malinichev
    23.07.2017 17:13
    +3

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


  1. nazarpc
    23.07.2017 17:44
    +39

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


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


    1. A-Stahl
      23.07.2017 18:23
      +1

      >а чаще вообще 1.
      Но-но-но! Имена некоторых однобуквенных переменных уже стали нарицательными. Попробуйте обозначить координату не через x,y,z или счётчик не через i,j,k и вы увидите много непонимающих взглядов:)


      1. nazarpc
        23.07.2017 18:26
        +5

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


      1. vlanko
        23.07.2017 20:56
        +5

        я работаю сейчас с географическими/прямоугольными координатами. Там х и у наоборот. И часть библиотек написано математиками(х вправо, у вверх), а часть геодезистами(наоборот). Боюсь, мне захочется назвать их сложнее :))
        А z там есть, но спрятан.


        1. radium
          23.07.2017 22:36
          +1

          О как это знакомо! Вероятно это будет fi и la (? и ?) или lat и lon.


          1. khim
            23.07.2017 23:22

            Вероятно это будет fi и la (? и ?) или lat и lon.
            А почему не ? и ??


            1. Krypt
              24.07.2017 00:04
              +5

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


            1. radium
              24.07.2017 00:14
              +5

              Если мы говорим о мейнстриме (C++/C#/Java), то общая практика состоит в избегании применения не ASCII символов в коде. До сих пор бывают проблемы с кодировкой исходников, что приводит к нежелательным последствиям. А ещё можно представить, что потом эти названия полезут в некое публичное API. А это накладывает дополнительное требование поддержки не ASCII-символов на все инструменты, которые с этим API работают, будь это линкеры для библиотек или OData/REST кодогенераторы для WebApi. Слишком большая проблема для ровного места :)


          1. domix32
            24.07.2017 10:26
            +5

            phi и lambda?


      1. Alyoshka1976
        23.07.2017 23:30
        +8

        Забавный пример из личной практики, связанный с именами переменных — когда в конце 80-х я осваивал программирование на МК-61 и МК-52, то одной из моих любимых книжек была книжка небольшого формата Очкова и Хмелюка «От микрокалькулятора к персональному компьютеру». Там были примеры программ как для микрокалькуляторов, так и на Бейсике. Меня в то время долго мучал вопрос — почему во многих программах фигурирует переменная с загадочным именем TEMP, никак не связанная с температурой ))) (cитуация усугублялась тем, что в школе я учил немецкий).


        1. ksil
          24.07.2017 15:59

          temporar (нем.)


          1. Source
            24.07.2017 23:32
            +1

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


      1. TerraV
        24.07.2017 13:26
        -6

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


        1. Free_ze
          24.07.2017 14:28
          +6

          Два вложенных фора c i/j очень часто на этапе написания содержат ошибку.

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


          1. mayorovp
            24.07.2017 14:33
            +5

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


          1. 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) — двумя или даже полными словами.


            1. Free_ze
              24.07.2017 15:23
              +4

              Тут действительно большая разница между i,j,k и хитромудрыми a,b,c,d, о семантике которых любой бы задумался. for и i — это как хлеб и рама.


              1. sasha1024
                24.07.2017 15:56

                Вы, наверно, имели в виду «хитромудрыми a, u, r, c».


                1. Free_ze
                  24.07.2017 16:29
                  +4

                  Их тоже, это не имеет значения на самом деле) Если в контексте имя переменной не дает стойкой ассоциации (как, например, r в геометрической формуле), то лучше воздержаться.

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

                  Использование i,j,k в качестве индексаторов — это общепринятая практика, а однобуквенные штуки намекающие на тип будут уместнее в foreach-циклах, где это не просто индекс, а экземпляр, обладающий состоянием и поведением.


                  1. Lex20
                    25.07.2017 20:37
                    -2

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


                    1. Free_ze
                      26.07.2017 10:18
                      +2

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


                      1. Lex20
                        26.07.2017 19:44
                        -2

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


                        1. Free_ze
                          27.07.2017 11:16
                          +3

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

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

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

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

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


            1. DistortNeo
              24.07.2017 15:35

              А если это шаблонный метод?


              1. sasha1024
                24.07.2017 16:02
                +1

                И? В чём разница?

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

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


                1. DistortNeo
                  24.07.2017 16:07

                  И? В чём разница?

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


                  1. sasha1024
                    24.07.2017 16:15
                    -1

                    i и o — очевидно же.


            1. vbif
              24.07.2017 15:47
              +2

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

              Другое дело — функции, делающие что-то тривиальное, когда кроме «v1», «v2» сложно выдумать что-то подходящее. «firstValue/secondValue»?


              1. sasha1024
                24.07.2017 16:09
                -1

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


                1. vbif
                  24.07.2017 16:41
                  +1

                  Огромная. В том, что здесь мы имеем дело не с абстрактным «числом от одного до десяти», а с конкретным элементом конкретного списка. И логично элемент списка «addresses» назвать «address», «users» — «user» и т.д.


                1. Free_ze
                  24.07.2017 16:46

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


                1. mayorovp
                  24.07.2017 19:08
                  +1

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


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


        1. vbif
          24.07.2017 15:05
          +3

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


        1. zenkz
          24.07.2017 17:35
          +4

          Зря минусите человека.

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


    1. KoCMoHaBT61
      23.07.2017 18:47
      -2

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

      Где-то есть золотая середина.


      1. radium
        23.07.2017 22:45
        +17

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


        1. KoCMoHaBT61
          24.07.2017 08:40

          Функция была странная, а термины «Logical» и «Physical» в этом контексте никогда не употреблялись.
          Тело цикла стало больше, но в силу того, что параметры никуда не передаются можно отследить логику работы.


          1. akzhan
            24.07.2017 10:02
            +10

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


      1. sumanai
        24.07.2017 16:46
        +3

        getAndConvertPhysicalToLogicalValue

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


        1. KoCMoHaBT61
          24.07.2017 17:18
          +1

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


          1. Color
            24.07.2017 17:34
            +1

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


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


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


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


    1. Alex_ME
      23.07.2017 19:45
      +4

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


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


      1. nazarpc
        23.07.2017 19:48
        +6

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


        1. d-stream
          23.07.2017 19:54
          +1

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


        1. lovermann
          23.07.2017 23:34
          +3

          Вот сейчас это был очень популистский камент :)


      1. SirEdvin
        23.07.2017 20:01
        +2

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


        1. ozonar
          24.07.2017 09:58
          +2

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

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


          1. mayorovp
            24.07.2017 10:04
            +1

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


            1. ozonar
              24.07.2017 10:11
              +4

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

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


              1. mayorovp
                24.07.2017 10:22
                +1

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


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


                1. aamonster
                  24.07.2017 11:19

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


                1. MacIn
                  24.07.2017 12:48

                  То, что в этом случае метод имел бы имя длиной символов в 50. Для комментария-то это нормально…


                  1. khim
                    24.07.2017 13:43
                    +1

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


                    1. quantum
                      24.07.2017 14:54
                      +1

                      На помощь приходит ide, которая в подсказке указывает описание метода и параметров


                      1. khim
                        24.07.2017 15:03
                        -1

                        Вы читаете код, тыкая в каждую функцию и читая описание метода и параметров? Мне вас жаль.

                        На практике код приходится читать гораздо чаще, чем писать, так что важно оптимизировать именно скорость чтения кода.


                    1. sasha1024
                      24.07.2017 15:14

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


                      1. michael_vostrikov
                        24.07.2017 20:38

                        use function cos as c;
                        echo c(0);  // 1
                        
                        var getById = document.getElementById;
                        console.log(getById);  // function getElementById() { [native code] }


                    1. MacIn
                      24.07.2017 20:13

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


                  1. Free_ze
                    24.07.2017 15:28

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


                    1. MacIn
                      24.07.2017 20:15
                      +1

                      Ок, пусть это не метод, а просто функция.

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

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


                      1. Free_ze
                        25.07.2017 11:59

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

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

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


                  1. vsapronov
                    24.07.2017 16:47
                    +1

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

                    Можно, конечно, обложить его комментариями, если это позволяет заснуть без угрызений. Но Мартин рекомендует выделить метод: Extract Method. И тогда методов бодет больше, они будут проще и не нуждаться в коментарии, потому что все что можно написать в коммент будет в коротком названии метода.


                    1. michael_vostrikov
                      24.07.2017 17:34
                      +2

                      … и будет нифига непонятно, как они работают в целом) Надо все-таки соблюдать баланс.


                      1. 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% случаев с которыми я сталкивался комментарии — неудачная попытка объяснить говённый код. И вместо того, чтобы плодить неконтролируемый компилятором, быстро устаревающий текст, нужно подумать о дизайне кода и исправить его.


                        1. michael_vostrikov
                          24.07.2017 18:53

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


                    1. MacIn
                      24.07.2017 20:22
                      +1

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

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

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

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


                      1. vbif
                        24.07.2017 22:42

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


                      1. vsapronov
                        25.07.2017 00:17

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


                      1. Bonart
                        25.07.2017 02:00

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

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


                        1. michael_vostrikov
                          25.07.2017 09:48
                          -1

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


                          1. Bonart
                            26.07.2017 02:07

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


                            1. michael_vostrikov
                              26.07.2017 06:40
                              -1

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


                      1. Free_ze
                        25.07.2017 12:04

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

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


          1. SirEdvin
            24.07.2017 11:04
            +1

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


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


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


          1. khim
            24.07.2017 13:41
            +1

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


            1. DistortNeo
              24.07.2017 15:16

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


              1. vsapronov
                24.07.2017 16:51
                +2

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

                Пишите говнокод (не важно по каким причинам) — перестаньте как можно скорее. Комментарии вообще не помогут.


        1. domix32
          24.07.2017 10:30

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


          1. vsapronov
            24.07.2017 16:54

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


            1. domix32
              24.07.2017 20:42

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


              1. vsapronov
                25.07.2017 19:50
                +1

                Понимаю. Это тяжелая работа — стать хорошим программистом. Знать хоть как-то английский — это всего лишь 10% (если не меньше) успеха. Чтобы его знать, надо прикладывать много усилий. Если человек не готов их прикладывать, например учить его в свободное от работы время, платить за эффективные занятия one-on-one с носителем, и т.д. Прикладывать усилия, понимаете — а не ныть и ждать когда школа/институт/работа научит. Если человек не хочет прикладывать такие усилия, то быть ему быдлокодером… Не потому что не знает английского, а потому что не умеет/не может самостоятельно прикладывать усилия в учебе. А программирование — это сплошная учеба.

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


            1. kuznetsovin
              25.07.2017 09:41

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


              1. Free_ze
                25.07.2017 12:15
                +2

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


              1. sasha1024
                25.07.2017 15:19
                +2

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


              1. sentyaev
                25.07.2017 15:39
                +1

                Конечно английский учить не нужно. Пишите на 1С там же русский. Это конечно сарказм.

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


              1. sasha1024
                25.07.2017 15:58

                Извините, если высказался слишком категорично.


              1. vsapronov
                25.07.2017 19:57

                Это не только вопрос знания как такового. Это вопрос адекватности и способности учиться.

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

                Способность учиться: на самом деле не так уж сложно выучить язык на достаточном уровне. Нужно просто приложить усилие и не быть уж совсем дебилом. Теперь, сюрприз-сюрприз, умение учиться — необходимый навык в программировании. Если вы не можете себя заставить выучить язык, то и программист вы, скорее всего, не очень…


              1. sasha1024
                25.07.2017 21:59

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

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


      1. grossws
        23.07.2017 20:11

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


      1. MadJackal
        23.07.2017 20:39
        +2

        Скорее всего, Вы просто сначала думаете, что должен сделать данный программный объект, начерно проектируете его структуру и только потом приступаете к наполнению кодом. К сожалению, правило «Сначала пойми, что ты хочешь написать и только потом начинай кодировать» нынче не сильно в чести…


        1. Xandrmoro
          23.07.2017 22:25

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


          1. MadJackal
            24.07.2017 00:20
            +1

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


      1. khim
        23.07.2017 21:51
        +9

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

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

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

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

        Почему среди OpenSource преобладают люди первого вида, а «за деньги» — в основном работают люди второго вида я не знаю…


        1. Old_Chroft
          23.07.2017 22:12
          +1

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


          1. khim
            23.07.2017 23:27
            +4

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

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

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

            Но сам принцип — «комментарий == сноска с пояснением в книге» для меня по прежнему является основным…


          1. TheOleg
            24.07.2017 20:03

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


          1. Bonart
            25.07.2017 02:05

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


        1. vsapronov
          24.07.2017 17:00
          +1

          Потому что люди которые пишут «за деньги» не особо интересуются программированием — многие из них, не все, разумеется. Они не живут кодом, не могут понимать язык программирования как человеский, хотя ЯП намного проще по выразительности. Они просто приходят к 9 и уходят в 6. Соответственно и код они производят так себе, который очень трудно понять, ну и лёкий способо это «исправить» — понаписать везде текста, вместо того, чтобы исправить дизайн кода — «уходить же в 6!»…


          1. Free_ze
            24.07.2017 17:05
            +3

            Они просто приходят к 9 и уходят в 6.

            Вы так говорите, будто это что-то плохое.


            1. vsapronov
              24.07.2017 18:39
              +2

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


              1. Free_ze
                24.07.2017 19:07
                +1

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


                1. vsapronov
                  24.07.2017 20:21

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

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

                  Я, обычно, рефакторю только те куски, которые нужны мне, если сотрудники или босс явно не просят помощи. Есть большая разница: выправлять свой свежий код, или пытаться потушить пожар из-за говнокода годовалой тухлости. Моя стратегия: чужие косяки идентифицировать и доводить до сведения, не использовать их в своем коде, не фиксить их проактивно, ждать пожара. Когда пожар случится, скорее всего поступает запрос: «пофикси, пожалуйста, проект горит, код говно и т.п.». В ответ надо выдать эстимейт достаточный для хорошего рефакторинга
                  или полного переписывания говнокода.

                  Опять же, речь была не о часах по факту. А о менталитете, в котором пребывание на рабочем месте важнее, чем выполненная работа и ее качество. У нас так говорят в конторе: «9 to 6 developers»… Это не значит, что остальные 24/7 вообще.


                  1. Free_ze
                    25.07.2017 12:22

                    не фиксить их проактивно, ждать пожара… В ответ надо выдать эстимейт достаточный для хорошего рефакторинга

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

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

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


                    1. vsapronov
                      25.07.2017 20:35

                      Хитрая тактика. Я пришел к этому спустя 10 лет спасения утопающих против их воли. Это было всегда больно для всех участников процесса. А потом я понял, что я пытаюсь хакнуть рынок. Рыночные правила работают так: тот кто делает плохой продукт рано или поздно потонет. Некоторые программисты проихводят плохой код — не надо им мешать, пусть рынок вмешается и они пойдут ко дну. Нужно просто оказаться рядом.
                      С таким подходом сложно не стать мудаком. Потому как есть люди, которые понимают, что что-то не так но не знают как/у кого попросить помощи. Т.е. они как бы и не против улучшаться, но не знают как начать. Ждать когда они потонут — аморально…

                      Начальство разное бывает. В реальности, у них дихотомия неслабая.
                      С одной стороны по договору найма (или его аналогу в завсисимости от страны) вам платят за часы работы. Т.е. вы должны работать с 9 до 6 там и даже часто оговаривается как оплачиваются переработки. Чаще же всего резльтат важен вне зависимости от того за какое время он был достигнут. Но начальство не может сказать: сделал работу — иди домой, очень скоро они обнаружат пустые офисы, а постоянное доступное присутствие подчиненных — важный атрибут их власти. Вобщем сложно.


                      1. Alexeyslav
                        26.07.2017 08:39

                        Пустые офисы? Врятли… если работа настолько проста… просто подкинет ещё в топку, и так до того уровня на котором работники будут едва справляться и постоянно загружены. Но тут возникнет другая проблема — люди в таких условиях быстро выгорят и перестанут работать вообще.


      1. FreeMind2000
        23.07.2017 22:02

        У ЧакНориса есть такое выражение — «комментарии плохо пахнут».
        Т.е. надо писать код так, чтоб комментарии вообще не требовались… Ибо часто бывает, что поменяв код или скопировав — комментарий остается без изменения и может обманывать программиста. Плюс такого подхода — названия переменных и функций должны быть гипер-продуманными и понятными. Ну и кода без комметариев на мониторе больше видно.

        Видимо, в open source кадый мнит себя ЧакНорисом :)


        1. ainoneko
          24.07.2017 07:37
          +2

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

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


          1. Old_Chroft
            24.07.2017 08:32
            +6

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


            1. khim
              24.07.2017 13:49

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

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


              1. alix_ginger
                24.07.2017 16:39
                +1

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


                1. khim
                  24.07.2017 17:09

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


              1. mayorovp
                24.07.2017 19:18

                Да никакого! Мне важно как оно здесь и сейчас работает!

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


                1. khim
                  24.07.2017 19:27

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

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


                  1. mayorovp
                    24.07.2017 19:35

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


                  1. michael_vostrikov
                    24.07.2017 19:55

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


                    1. khim
                      24.07.2017 20:16

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

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


                      1. michael_vostrikov
                        24.07.2017 20:30

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


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

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


        1. michael_vostrikov
          24.07.2017 08:29
          +1

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


          1. khim
            24.07.2017 13:53
            +1

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

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

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


            1. michael_vostrikov
              24.07.2017 14:19
              +2

              Этого невозможно заметить, если вы не читаете код, а читаете только комментарии.

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


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

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


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

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


              1. khim
                24.07.2017 14:47
                +1

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

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


                1. michael_vostrikov
                  24.07.2017 15:34
                  +1

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


                  1. khim
                    24.07.2017 17:11
                    +2

                    Почитайте топикстартера: Бывает, пытаешься понять что-то в какой-то OpenSource библиотеке, открываешь какой-нибудь исходный файл и не видишь ни единого комментария, кроме лицензии, и хорошо, если это 500 строк, а не 5000.

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


                    1. michael_vostrikov
                      24.07.2017 17:37

                      Бывает, пытаешься понять

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


                      1. khim
                        24.07.2017 18:39
                        +2

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

                        Ориентироваться же по комментариям в коде — это всё равно что пытаться понять что есть в книге не по оглавлению, а по сноскам.


        1. akzhan
          24.07.2017 10:04
          +1

          Пример комментария к месту:


          https://github.com/python/cpython/blob/master/Python/pyhash.c#L34


          1. khim
            24.07.2017 13:54

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


      1. Amomum
        24.07.2017 12:31
        +2

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

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

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


        1. mayorovp
          24.07.2017 12:48

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


          1. khim
            24.07.2017 13:57

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


            1. mayorovp
              24.07.2017 14:01
              +1

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


          1. Amomum
            24.07.2017 14:08
            +1

            Так это и были умные указатели. Только на С. Поэтому ref/unref нужно вызывать руками.


        1. khim
          24.07.2017 13:57

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


          1. Amomum
            24.07.2017 14:16
            +1

            А как документация для glib поможет мне понять вот эту функцию из библиотеки, которая от glib зависит?

            const char *
            arv_device_get_string_feature_value (ArvDevice *device, const char *feature)


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

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


            1. khim
              24.07.2017 14:50
              +1

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


              1. Amomum
                24.07.2017 18:22

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

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


        1. BelBES
          24.07.2017 16:51
          +2

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


      1. andreysmind
        30.07.2017 13:05
        -1

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


    1. dkdkdk
      23.07.2017 23:30
      +2

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


      1. tandzan
        24.07.2017 03:19
        +1

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


    1. goral
      23.07.2017 23:30
      +6

      А я pk для себя расшифровываю как primaryKey (при работе с БД)


    1. Alexeyslav
      24.07.2017 09:11

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


      1. nazarpc
        24.07.2017 09:52
        +2

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


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


        1. MacIn
          24.07.2017 12:52
          +1

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


      1. arvitaly
        24.07.2017 13:17

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


        1. Alexeyslav
          24.07.2017 22:43

          И поддерживать потом обе версии?


  1. x893
    23.07.2017 20:06

    Самое хреновое, когда писатели такого кода начинают учить (или заставлять) других как надо писать.
    Комментарии конечно вещь полезная, но тут всё зависит от ситуации.


  1. firk
    23.07.2017 20:35
    +6

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

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


    1. sasha1024
      24.07.2017 13:04

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


  1. alan008
    23.07.2017 20:46
    -2

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

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


    1. SirEdvin
      23.07.2017 21:04
      -3

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


      1. MadJackal
        23.07.2017 21:21
        -4

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


      1. nazarpc
        24.07.2017 09:54
        +1

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


      1. MacIn
        24.07.2017 12:53
        +1

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


    1. grossws
      23.07.2017 21:06
      +12

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


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


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


      1. ivan19631224
        23.07.2017 22:17
        +1

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


      1. alan008
        23.07.2017 22:37
        +1

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


        1. grossws
          23.07.2017 22:44
          +1

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


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


          1. Krypt
            24.07.2017 00:18

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


            1. grossws
              24.07.2017 00:50
              +1

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


              1. khim
                24.07.2017 01:37
                +2

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


                1. grossws
                  24.07.2017 01:40

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


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


                  1. khim
                    24.07.2017 01:47

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

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


                    1. grossws
                      24.07.2017 01:55

                      Экий вы оптимист, батенька…

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


                  1. Krypt
                    24.07.2017 08:35
                    -1

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


                    1. domix32
                      24.07.2017 10:43
                      +1

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


                      1. Krypt
                        24.07.2017 22:37

                        Вот именно поэтому я и не пишу игры.
                        А вообще это был сарказм.


          1. DarkEld3r
            01.08.2017 17:08

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

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


            1. grossws
              01.08.2017 17:12

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


              1. DarkEld3r
                01.08.2017 17:35

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


        1. khim
          23.07.2017 23:30

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


      1. michael_vostrikov
        23.07.2017 22:58

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


        1. grossws
          23.07.2017 23:19
          +1

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


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


          1. michael_vostrikov
            24.07.2017 08:26
            -1

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


            1. Free_ze
              24.07.2017 12:29
              +1

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

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


            1. khim
              24.07.2017 14:04
              +3

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

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

              Много вы программ видели, которые ничего этого не содержат? Или где такого кода очень мало? Я видел — это обычно 100500 обёрток, которые и без всякого UB тормозят так, что им никакой компилятор не поможет…


              1. michael_vostrikov
                24.07.2017 14:25

                a = b + c
                Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению. А не запускает форматирование жесткого диска. Это я и имел в виду под «генерировать максимально близкий машинный код».


                1. vbif
                  24.07.2017 15:29

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


                1. khim
                  24.07.2017 17:16
                  +1

                  Тем не менее, компилятор генерирует для этого выражения что-то вроде add eax, ebx или аналогичного ему по поведению.
                  Простейший компилятор — да, может быть. Да и отптимизирующий тоже, если это выражение присутствует в отдельной функциии и ничего другого там нет — но кому такая функция нужна?

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


                  1. michael_vostrikov
                    24.07.2017 17:43

                    И как ни странно, это тоже «максимально близкий машинный код», потому что дает тот результат, который требуется — в a будет сумма b и c. Даже если a появляется только во внутренних регистрах процессора при вычислении сложной адресации.


                    1. khim
                      24.07.2017 18:48
                      +1

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

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

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


                      1. michael_vostrikov
                        24.07.2017 19:07

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


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

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


                        1. mayorovp
                          24.07.2017 19:23
                          +1

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


                          1. michael_vostrikov
                            24.07.2017 19:46
                            -2

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


                        1. khim
                          24.07.2017 19:37
                          +2

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

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

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

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

                          О самых вопиющих случаях компиляторы сообщают.


                          1. michael_vostrikov
                            24.07.2017 19:54
                            -1

                            В том-то и дело, что может получить в результате оптимизаций.

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


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

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


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

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


                            1. vbif
                              24.07.2017 19:58
                              +1

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


                            1. khim
                              24.07.2017 20:22
                              +1

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

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


                            1. Salabar
                              24.07.2017 23:15
                              +2

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


                              1. michael_vostrikov
                                25.07.2017 07:15
                                -1

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


                                1. vbif
                                  25.07.2017 08:43
                                  +2

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


                                  1. michael_vostrikov
                                    25.07.2017 09:17
                                    -3

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


                                    1. vbif
                                      25.07.2017 09:53
                                      +2

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


                                      1. michael_vostrikov
                                        25.07.2017 12:13
                                        -3

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


                                        1. vbif
                                          25.07.2017 12:22
                                          +2

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


                                          1. khim
                                            25.07.2017 13:55

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

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

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


                                            1. grossws
                                              25.07.2017 15:29

                                              Если у вас в программе написано два раза "mov eax, ebx" (вот прямо подряд, без зазоров), Их вроде бы можно заменить на один mov.

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


                                            1. michael_vostrikov
                                              25.07.2017 15:38
                                              -3

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

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


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

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


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

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


                                              1. vbif
                                                25.07.2017 15:47

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


                                                1. michael_vostrikov
                                                  25.07.2017 16:20

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


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

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


                                              1. khim
                                                25.07.2017 16:41
                                                +2

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

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

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

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

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

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


                                                1. michael_vostrikov
                                                  25.07.2017 17:12
                                                  -1

                                                  Ничем — и в этом-то всё и дело.

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


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

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


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

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


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

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


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

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


                                                  1. 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. Только не надо про то, что «стало только лучше» и «скорее всего программист этого и хотел». Эти критерии — неформализуемы и их, в общем, невозможно использовать для того, чтобы решать — является ли что-то хорошей оптимизацией или нет.


                                                  1. vbif
                                                    25.07.2017 18:09
                                                    +3

                                                    Оптимизации бывают не только из-за UB

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

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

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


                                                    1. khim
                                                      25.07.2017 18:20
                                                      +1

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

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

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

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


                                                    1. michael_vostrikov
                                                      25.07.2017 19:47
                                                      -3

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

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


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

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


                                                      1. khim
                                                        25.07.2017 20:09
                                                        +1

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

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


                                                      1. vbif
                                                        25.07.2017 23:07

                                                        Тот же «исходный код» может быть библиотекой, которая используется в 100500 разных проектах. И та или иная часть её может никогда не понадобиться. И без соответствующих оптимизаций придётся либо при каждом вызове проверять кучу условий, которые всегда не выполняются, крутить ненужные циклы и пожертвовать 90% скорости на ненужный мусор. Либо нагородить 100500 вызовов, делающих одно и то же, но чуть по-разному.


                                          1. 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, это будет известно на этапе компиляции, и можно показать предупреждение.


                                            1. vbif
                                              25.07.2017 15:59
                                              +1

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


                                              1. michael_vostrikov
                                                25.07.2017 16:24
                                                -1

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


                                                1. vbif
                                                  25.07.2017 16:38

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

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


                                                  1. michael_vostrikov
                                                    25.07.2017 17:19

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


                                                    1. khim
                                                      25.07.2017 17:24

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


                                                      1. michael_vostrikov
                                                        25.07.2017 19:27

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


                                                        1. khim
                                                          25.07.2017 20:19
                                                          +1

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

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

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

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


                                            1. 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 и должен оставлять его как есть). Одновременно.


            1. 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 программистом) вы получите работу инкремента за одну инструкцию.


              1. michael_vostrikov
                24.07.2017 18:39
                -1

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

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


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

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


                1. grossws
                  24.07.2017 18:56
                  +1

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

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


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


                  1. michael_vostrikov
                    24.07.2017 19:12
                    -2

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


                1. khim
                  24.07.2017 19:04
                  +2

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

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

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

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

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

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


                  1. 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


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


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


                    1. vbif
                      24.07.2017 19:33
                      +2

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


                      1. khim
                        24.07.2017 19:46

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


                        1. vbif
                          24.07.2017 19:51

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


                      1. michael_vostrikov
                        24.07.2017 19:58

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


                        1. khim
                          25.07.2017 02:17
                          +1

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

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

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

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


                          1. michael_vostrikov
                            25.07.2017 07:21
                            -3

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


                            1. 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. Если второе — то всё ещё остаётся вопрос какие оптимизации являются хорошими.


                              1. michael_vostrikov
                                25.07.2017 19:43
                                -3

                                Ожидаемого кем? Ожидаемого когда?

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


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

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


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

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


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

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


                                1. khim
                                  25.07.2017 20:53
                                  +1

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

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

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

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

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

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

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

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

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

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


                                  1. michael_vostrikov
                                    25.07.2017 22:15
                                    -1

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

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


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

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


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


                                    1. vbif
                                      25.07.2017 23:14
                                      +3

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


          1. akzhan
            24.07.2017 10:30
            -1

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


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


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


            1. Free_ze
              24.07.2017 12:49
              +3

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

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


            1. khim
              24.07.2017 14:07
              +3

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

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


              1. DistortNeo
                24.07.2017 15:44

                В языках без GC избавиться от UB черезвычайно сложно.

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


                1. khim
                  24.07.2017 17:20
                  +1

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

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


                  1. mayorovp
                    24.07.2017 19:29

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


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


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


                    1. khim
                      24.07.2017 19:53
                      +1

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

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

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

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


                      1. mayorovp
                        24.07.2017 20:02

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


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

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


                        Или я не прав?


                        1. khim
                          24.07.2017 20:37
                          +1

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

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

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


    1. Andrey2008
      23.07.2017 23:29
      +4

      А что тут комментрировать… Да, язык ужасно сложный и коварный. Но уж какой есть. Приходится использовать вспомогательные инструменты, такие как PVS-Studio.

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

      Выдавать предупреждения надо очень аккуратно. И многие предупреждения мы не делаем именно по той причине, что будет слишком много ложных срабатываний (см. Философия статического анализатора PVS-Studio). А когда делаем какие-то диагностики, то стараемся учесть эмпирические нюансы, которые подсказывают есть ошибка или нет.


  1. Old_Chroft
    23.07.2017 21:40
    +11

    Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте
    Фразе больше 20 лет. С неё и надо начинать учить программирования. А за code-ниндзюцу, приведенное в статье, надо прищемлять пальцы дверью, что бы больше такого написать не смог уже никогда. Ну, если это не «патч Бармина» и тому подобные вещи — но они и сделаны для того, чтобы их сложно было прочитать.


    1. Temtaime
      24.07.2017 00:54
      -5

      Если мой ЯП строго определяет порядок вычислений и всё описанное в статье чётко определено — почему это означает то, что я пишу говнокод?


      1. lastrix
        24.07.2017 03:40
        +6

        1. Не все знают стандарты наизусть. Более того, однажды выучив, через какое-то время удивишься, что часть уже забыл или исказил;
        2. Нужно думать гораздо больше, чем ожидается от среднего по больнице;
        3. Отладчики работают почти всегда построчно, а если и есть возможность по операторам, то сделать это проблематично в плане интерфейса — неудобно;
        4. Порядок выполнения задает номер строки или скобочки, но никак не стандарты — вот правило, которое должен знать каждый программист.


        1. sasha1024
          24.07.2017 13:21

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

          Я не распарсил.


          1. khim
            24.07.2017 14:09

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


            1. sasha1024
              24.07.2017 14:18

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

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

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


              1. sasha1024
                24.07.2017 14:33

                Считать коммент удалённым.


            1. sasha1024
              24.07.2017 14:24

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

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

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


        1. vbif
          24.07.2017 19:43

          Не на тот комментарий ответил, см. ниже.


      1. nApoBo3
        24.07.2017 10:46
        +1

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


      1. vbif
        24.07.2017 19:26

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

        Лучше без нужды на это правило не полагаться.


    1. s-kozlov
      28.07.2017 19:55
      -1

      С неё и надо начинать учить программирования.


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


  1. geher
    24.07.2017 09:20
    +1

    Порядок вычисления выражений...

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


  1. AllexIn
    24.07.2017 10:37

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

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


    1. sasha1024
      24.07.2017 13:27

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


  1. erwins22
    24.07.2017 12:05

    А если спросят на собеседовании?
    Сказать, что разработчики С# тоже не знают ответа на этот вопрос?


    1. mayorovp
      24.07.2017 12:50
      +3

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


      1. erwins22
        24.07.2017 14:53
        -5

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

        У меня знакомый когда устраивался С++ разработчиком после 3-4 собеседований учил нечто вроде

        for(int = i;i-- < 6;++i++)
        


        1. DistortNeo
          24.07.2017 15:59
          +4

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


        1. AllexIn
          24.07.2017 16:00
          +1

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

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


        1. Free_ze
          24.07.2017 16:16
          +2

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

          Компетенция программиста — навык писать поддерживаемый код. Учить это смысла нет вообще никакого, хотя для C++ стоило бы поинтересоваться, почему так писать неправильно.


  1. grieverrr
    24.07.2017 15:05
    -4

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


    1. sasha1024
      24.07.2017 15:48

      Моё мнение.

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


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


      1. Dima_Sharihin
        25.07.2017 08:00

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


        1. sasha1024
          25.07.2017 08:11

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

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


          1. Dima_Sharihin
            25.07.2017 09:03

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


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

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


            1. sasha1024
              25.07.2017 09:18

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

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


    1. Oxoron
      24.07.2017 15:56

      В циклах for хорошо смотрится. Лучше чем лямбда.


      1. DrLivesey
        24.07.2017 16:49

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


  1. YoungSkipper
    24.07.2017 17:52
    -1

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


    Этот код вызовет предупреждение (если вызовет) в не зависимости от того помечен ли метод calculate_tax константным или нет. Компилятору вообще практически всегда наплевать на ваши const определения.


  1. guai
    25.07.2017 00:52
    +1

    У автора в тексте повторное использование переменной Джо :)


  1. andreysmind
    30.07.2017 13:11
    -1

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