company_banner

Прочти меня: код, который не выбесит соседа



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

    Я расскажу о подходах, которые мы используем в Яндекс.Такси для написания читаемого кода на C++, Python, JavaScript и других языках.

    Обычный рабочий процесс


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

    Выглядит функция sample как-то так:

    std::string sample(int d, std::string (*cb)(int, std::string)) {
      // Getting new descriptors from d with a timeout
      auto result = get(d, 1000);
    
      if (result != -13) {
        // Descriptor is fine, processing with non bulk options
        auto data = process(result, true, false, true);
    
        // Passing processing result to the callback
        return cb(data.second.first, data.second.second);
      }
    
      // Notifying callback on error
      return cb(result, {});
    }
    

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

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

    Решаем заменить часть int на отдельный тип данных Descriptor в надежде, что компилятор сам найдёт ошибки и нам не придётся дальше отлаживать код. Зовём автора кода, просим его подсказать, где дескрипторы, а где числа. Он сейчас работает над другим проектом, но после долгих уговоров неохотно помогает и быстро ретируется:

    enum class Descriptor : int {};
    
    std::string sample(Descriptor d, std::string (*cb)(Descriptor, std::string)) {
      // Getting new descriptors from d with a timeout
      auto result = get(d, 1000);
    
      if (result != Descriptor(-13)) {
        // Descriptor is fine, processing with non bulk options
        auto data = process(result, true, false, true);  // <== ERROR
    
        // Passing processing result to the callback
        return cb(data.second.first, data.second.second);  // <== ERROR
      }
    
      // Notifying callback on error
      return cb(result, {});
    }
    

    И тут понеслось:

    • Компилятор нашёл сразу две ошибки. Это очень подозрительно, может, мы не так типы расставили?
    • А что вообще такое data.second.first и data.second.second? В комментариях не написано.

    Делать нечего, придётся прочитать весь код и комментарии, чтобы понять, как исправить ошибки.

    Боль


    Поначалу казалось, что много комментариев — это хорошо. Однако при отладке всё выглядит иначе. Код написан на двух языках: на английском и C++. Когда мы пытаемся понять, что происходит в коде, и отладить его, нужно прочитать английский, перевести его в голове на русский. Затем прочитать код на C++, его тоже перевести в голове на русский и сравнить эти два перевода. Убедиться, что смысл комментария совпадает с тем, что написано в коде, а если код делает что-то другое, то, возможно, там и кроется ошибка.

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

    Попробуйте угадать, что значат булевые флажки true, false, true в функции process? В комментариях о них ни слова. Чтобы разобраться, нужно пойти в header file, где объявлена функция process:

    std::pair<Descriptor, std::pair<int, std::string>> process(bool, Descriptor, bool, bool);
    

    … И там мы увидим, что у булевых переменных нет осмысленных имён.

    Чтобы понять, что происходит, нужно перейти в соседний файл, прочитать код функции process и разобраться в нём. Возможно, походить по соседним функциям и почитать их. На исследование смежных файлов тратится уйма времени, что мешает осознанию функции sample и портит настроение.

    Наконец, data.second.first и data.second.second. Чтобы выяснить их назначение, нужно отмотать назад — туда, где мы получаем переменную data. Пойти в место, где объявлена функция process, увидеть, что комментариев нет, а process возвращает пару от пары. Пойти в исходники, узнать, что обозначают переменные int и string, — и на всё это снова уходит очень много нашего времени.

    Ещё одна маленькая боль — код обработки ошибок перемешан с основной логикой. Это мешает ориентироваться. Обработка ошибок функции sample находится внизу, а в середине, внутри блока if, с большими отступами находится happy path.

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

    Выжимка проблем


    • Код написан на двух языках:
      – Его в два раза больше.
      – При отладке возникают проблемы со сверкой двух языков.

    • Комментариев всё ещё недостаточно:
      – Приходится читать код смежных функций.
      – Есть магические константы.

    • Код обработки ошибок и основной логики перемешаны:
      – Большие блоки кода с большими отступами.

    Читаемый код


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

    Поехали:

    std::string Sample(Descriptor listener,
                       std::string (*on_data)(Descriptor, std::string))
    {
      UASSERT_MSG(on_data, "Callback must be a non-zero function pointer");
    
      const auto new_descriptor = Accept(listener, kAcceptTimeout);
      if (new_descriptor == kBadDescriptor) {
        return on_data(kBadDescriptor, {});
      }
    
      auto data = Process(Options::kSync, new_descriptor);
      return on_data(data.descriptor, data.payload);
    }
    

    В первой же строчке проверяем входные параметры. Это мини-подсказка/документация по тому, какие данные ожидаются на входе функции.

    Следующая правка: вместо функции с непонятным именем Get появляется Accept, широко известная в кругах сетевых программистов. Затем страшную константу 1000 превращаем в именованную константу с осмысленным читаемым именем.

    Теперь строка прекрасно читается без дополнительных комментариев: из listener мы принимаем новый дескриптор, на эту операцию даётся kAcceptTimeout.

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

    Обработка ошибок также происходит сразу. Получили значение — тут же проверяем его на невалидность. За счёт этого вложенность становится меньше, код становится чуть компактнее.

    Специфика C++
    Маленький бонус — большинство компиляторов в C++ считают одиночные if без блока else холодным путём.

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

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

    Дальше. Функция process преобразовалась. Вместо true, false, true теперь есть перечисление возможных опций для process. Код можно прочитать глазами. Сразу видно, что из дескриптора мы процессим какие-то данные в синхронном режиме и получаем их в переменную data.

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

    В результате код стал почти в два раза короче. Он стал понятнее. Больше не нужно ходить в соседние функции и файлы. Читать такой код куда приятнее.

    Приёмы


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

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

    1) Compute(payload, 1023) — нечитаемо. Что такое 1023?
    Используйте именованные константы.
    Compute(payload, kComputeTimeout)

    Альтернативным решением может быть явное использование имён параметров. Например, Python позволяет писать:

    Compute(payload=payload, timeout=1023)

    Ну и C++20 не отстаёт:

    Compute({.payload=payload, .timeout=1023});

    Идеальный результат получается, если пользоваться сразу двумя приёмами:

    Compute(payload=payload, timeout=MAX_TEST_RUN_TIME);

    2) Compute(payload, false) — нечитаемо. Что такое false?
    Используйте перечисления или именованные константы вместо bool.
    У bool не всегда понятна семантика. Введение перечисления даже из двух значений явно описывает смысл конструкции.

    Compute(payload, Device::kGPU)

    Именованные аргументы в этом месте не всегда спасают:

    Compute(payload=payload, is_cpu=False);

    Всё ещё непонятно, что False заставляет считать на GPU.

    3) Compute(data.second.first, data.second.second) или Compute(data[1][0], data[1][1]) — что вообще тут происходит?
    Используйте типы с информативными именами полей, избегайте кортежей.
    Compute(data.node_id, data.chunk_id)

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

    Попробуйте угадать, какой смысл у возвращаемых int и std::string в коде.

    std::tuple<int, std::string> Receive();

    int — это дескриптор устройства? Код возврата?

    А вот так всё становится кристально ясно:

    struct Response {
        int pending_bytes;
        std::string payload;
    };
    Response Receive();

    4) void Accept(int , int); — что это за два числа?
    Заводите отдельные типы данных для разных по смыслу вещей.
    void Accept(Descriptor, std::chrono::milliseconds)

    Или для Python:

    def accept(listener: Descriptor, timeout: datetime.timedelta) -> None:

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

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

    5) void Compute(Data data) — функция есть в модуле или заголовке, но должны ли мы ей пользоваться?
    Используйте особый namespace или именование для служебных вещей.
    namespace detail { void Compute(Data data); }

    Или для Python:

    def _compute(data: Data) -> None:

    С namespace detail/impl или с особым наименованием пользователь поймёт, что функцию использовать не нужно.

    6) d, cb, mut, Get — что это?
    Придумывайте информативные имена переменных, классов и функций.
    descriptor, callback, mutator, GetDestination

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

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

    Так вот, через неделю или месяц будет сложно вспомнить, что такое d или cd, что делает метод Get (или это вообще класс Get?). Что он возвращает?

    Информативные имена вам очень помогут. При чтении будет сразу видно, где descriptor, callback и mutator, а где функция под именем GetDestination() возвращает какой-то Destination.

    7) connection = address.Connect(timeout) — отладчик завёл нас в эту строчку кода. Что там за переменные, откуда они и куда мы их присваиваем?
    Закрепите разные стили за переменными класса, аргументами функций и константами.
    Если закрепить отдельные стили за разными типами переменных, код читается лучше. В большинстве распространённых code style именно так и делают:

    connection_ = address.Connect(kTimeout);

    Мы сразу видим, что переменная address — локальная; что мы пытаемся соединиться с kTimeout, который является константой. Результат соединения присваиваем переменной класса connection_. Поменяли буквально пару символов, а код стал понятнее.

    Для Python стоит дополнительно придерживаться правила, что приватные поля начинаются с нижнего подчёркивания:

    self._connection = address.Connect(TIMEOUT);

    8) connection_ = address.Connect(timeout / attempts) — есть ли тут ошибка?
    Используйте assert, чтобы проверить, правильно ли используют ваш код.
    Если количество attempts будет равно нулю, то нативное приложение, скорее всего, рухнет. Где-то возникнет stack trace или корка. С помощью дополнительных телодвижений можно добраться до stack trace и понять, что падение произошло именно в этой строчке.

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

    Однако если внутри Connect не будет проверки, всё станет сильно-сильно сложнее. Приложение не упадёт, но будет работать неправильно, не так, как мы ожидаем.

    Коду явно не хватает проверок:

    ASSERT(attempts > 0);

    assert timeout > 0

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

    Assert не только позволяет быстро находить ошибки, но и добавляет читаемости. Выражение assert timeout > 0 прямо говорит, что код ниже будет работать неправильно с отрицательными числами и 0.

    8.1) connection_ = address.Connect(timeout / attempts) — есть ли тут ошибка?
    НЕ используйте assert для проверки пользовательского ввода.
    Будет невежливо (и опасно!), если ваша библиотека уронит весь сервер потому, что кто-то на сайте ввёл неправильное число. Здесь стоит использовать другие механизмы для сообщения об ошибках:

    if (attempts <= 0) throw NegativeAttempts();

    if (attempts <= 0) raise NegativeAttempts();

    Как отличить неправильное использование функции программистом от неправильного ввода?

    Вот несколько примеров:

    • функция init() должна быть вызвана перед первым использованием функции foo() — assert,
    • мьютекс не должен дважды захватываться из одного и того же потока — assert,
    • баланс на карте должен быть положительным — НЕ assert,
    • стоимость поездки не должна превышать миллиона доллларов — НЕ assert.

    Если не уверены — не используйте assert.

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

    ASSERT(attempts > 0);
    if (attempts <= 0) throw NegativeAttempts();

    9) v = foo(); bar(v); baz(v); assert(v); — а функциям bar() и baz() точно хорошо?
    Не тяните с обработкой ошибок, не несите их в блок else.
    Как только получили значение — сразу проверяйте и обрабатывайте ошибки, а дальше работайте как ни в чём не бывало.

    Такой подход поможет избежать излишней вложенности конструкций — за вашей мыслью будет проще следить.

    Пример:

    if (value != BAD) {
        auto a = foo(value);
        auto b = bar(value);
        if (a + b < 0) {
             return a * b;
        } else {
             return a / b + baz();
        }
    }
    
    return 0;

    Сравните:

    if (value == BAD) {
        return 0;
    }
    
    auto a = foo(value);
    auto b = bar(value);
    if (a + b < 0) {
         return a * b;
    }
    
    return a / b + baz();

    10) [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
    Придерживайтесь вменяемой вложенности конструкций.
    Если в вашем коде есть if, внутри которого for, внутри которого if, внутри которого while, — это сложно читать. Стоит разнести какие-то внутренности на отдельные функции и дать функциям осмысленные имена. Так код станет красивее и приятнее для чтения.

    11) Самая важная часть, самая большая хитрость, о которой мало где написано!
    Если вам хочется поставить комментарий...
    Попробуйте переделать код так, чтобы не хотелось.

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

    Вместо заключения


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

    Даже в рамках одной компании практики могут расходиться (например, в userver у нас пожёстче с наименованями и bool).

    Пожалуйста, расскажите о принятых у вас практиках в комментариях! Будет интересно обсудить и взять их на вооружение.

    UPD.
    Правило от dkuch:
    12) Сужать скоуп переменных до минимально возможного.
    Яндекс
    Как мы делаем Яндекс

    Комментарии 75

      +10
      в «улучшенном» коде
      if (new_descriptor == kBadDescriptor) {
          return cb(kBadDescriptor, {});
        }

      Все равно осталась переменная cb, хотя вроде автор сам ратует за осмысленные имена переменных.

      new_descriptor и kBadDescriptor — разве не нужно придерживаться единого стиля именования?
      Лучше так — if (newDescriptor == kBadDescriptor)

      И потом, не очевидно, что за загадочная 'k' именах kAcceptTimeout и kBadDescriptor? По мнению автора имя ft — не понятно, fTime — понятно, хотя яснее всего — fullTime. Мне кажется не должно быть даже частичных сокращений, кроме специфических для языка префиксов.
        0
        Спасибо! Сейчас поправлю cb на on_data

        Остальные два замечания — издержки Google C++ Style Guide. Это один из самых популярных стилей, поэтому он и использовался в примерах.
          0
          new_descriptor == kBadDescriptor
          И то дескриптор и то дескритптор. Какие такие издержки СтайлГайда? Значит стиль должен быть одинаковым.
            +6
            Один из них локальная переменная, другой — глобальная константа. Style Guide их различает и обязывает именовать по разному. Так же смотрите пункт 7) в статье.
          0

          Я бы еще предложим немного расширить интерфейс обработки ошибок Accept'а (для консистентности).


          Сейчас чтобы пользоваться функцией нужно:


          1. Знать с чем сравнивать (и как сравнивать) возвращаемое значение;
          2. Знать как в случае ошибки правильно вызывать коллбек, какие данные туда передавать, какие данные ожидают на той стороне.

          Как-нибудь так:


          const auto new_descriptor = Accept(listener, kAcceptTimeout);
          if(is_invalid_descriptor(new_descriptor))
          {
              auto data = ProcessError(new_descriptor);
              return on_data(data.descriptor, data.payload);
          }
          +5

          Позволю себе покритиковать полученный результат. Приведенный код все еще достаточно низкого качества для современного С++.


          Во-первых, колбеки так уже никто не пишет. Желательно вместо std::string (*cb)(Descriptor, std::string) писать std::function<std::string(Descriptor, std::string)>. Однако тут тоже непонятно, что такое std::string в аргументе.


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


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


          Ну и напоследок, смешение стилей именования переменных: через _ и camelCase.

            +2
            Ну и напоследок, смешение стилей именования переменных: через _ и camelCase.

            Думаю, это кодстайл ClickHouse (возможно, не только он) там имена переменных с _, а у функций camelCase
              +1
              Однако тут тоже непонятно, что такое std::string в аргументе
              Ну так к чему полумеры? Можно же написать:
              std::function<std::string(Descriptor connection, std::string name)>

              и сразу всё будет понятно.
                0

                ну, функция всего несколько строк, можно и так:


                template<class OnData>
                std::string Sample(Descriptor listener,
                                   OnData &&onData){
                ...
                }

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

                  +6
                  Желательно вместо std::string (*cb)(Descriptor, std::string) писать std::function<std::string(Descriptor, std::string)>.

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


                  Желательно сделать шаблонный параметр и обмазать его концептами, чтобы показать, что там ожидается такой-то callable.

                    –1
                    ну future не всегда возможно, а корутинами невозможно писать бизнес логику под мобильные платформы
                      0

                      А что не так с мобильными платформами?

                        0
                        как правило ndk и apple'ы немного позже начинают полную поддержку стандарта, чем его выход и чем это могло бы быть
                    –8
                    Допустим, вы работаете в компании, пишете код. Написали функцию, начинаете обкладывать её тестами и понимаете, что что-то глючит. Ну что ж, отлаживаем… Оказывается, что плохо работает не ваш код, а функция sample от другого разработчика, которую вы используете.

                    Если для тестирования и отладки своего куска кода надо лезть в чужой, то у вас с тестированием все очень сильно не так. А если "чужой код" будет вообще каким-то сторонним сервисом, то что тогда будете делать? В остальном, очевидно, все правильно, хотя можно было бы просто дать ссылку на книгу Макконнелла.

                      0
                      Для решения проблемы «В соседнем проекте бага» созданы целые сервисы, позволяющие удобно делать исправления. Например github, с сотнями тысяч разработчиков и проектами на подобие LLVM, GCC, CPython и т.п.

                      Если вы знаете, как писать большие проекты без ошибок — пожалуйста, расскажите.
                        0
                        Если вы знаете, как писать большие проекты без ошибок — пожалуйста, расскажите.

                        Этого я не знаю, но знаю, как тестировать свой код не завися при этом от багов в чужом :)

                          +1
                          тогда расскажите это
                      +2
                      Вложенные pair (да еще с итераторами на map) всегда причиняют боль, p.first.second и т.п. Но чаще всего спасает введение алиаса на него
                      auto &name = p.first.second

                      Еще мой перфекционизм страдает от того, что first — 5 букв, second — 6, из-за чего выравнивание кода немного ломается. Мне больше нравится имена из haskell у пары: p.fst, p.snd — коротко и ясно.
                        +21
                        Мне больше нравится имена из haskell у пары: p.fst, p.snd — коротко и ясно.


                        Глсн н нжн, вс и тк пнтн.
                          0
                          — Глсн н нжн
                          — Хрш тк гврть кгд н сть
                          — Хрш
                          +1

                          если уж так хочется читаемости без большого количества букв, то никто не мешает


                          Desciptor desk;
                          std::string payload;
                          std::tie(desk, payload) = Process(Options::kSync, new_descriptor);
                            return on_data(desk, payload);

                          или вообще по модному


                          auto [desk, payload] = Process(Options::kSync, new_descriptor);
                            return on_data(desk, payload);
                          +2
                          Маленький бонус — большинство компиляторов в C++ считают одиночные if без блока else холодным путём.

                          Что такое «холодный путь»? Термин не гуглится на просторах сети.
                            +1
                            hot path | горячий путь — код на критичном к производительности пути выполнения
                            cold path | холодный путь — код на не критичном к производительности пути выполнения
                              +2

                              Разве не правильнее говорить «срабатывает часто» или «срабатывает редко»? Говорить о влиянии джампа рядом с очевидно сетевым вызовом accept как-то странно.

                              +2

                              Вот, например, GCC генерирует код с одиночным if'ом таким образом, что мы прыгаем только если у нас ошибка (условие выполнено)
                              https://godbolt.org/z/MET3vq


                              Мы можем явно сделать if "горячим" пометив его атрибутом [[likely]] и мы будем прыгать тогда когда ошибки нет (условие не выполнено), тогда год будет таким:
                              https://godbolt.org/z/51c6oT

                                +1

                                Если бросается исключение, или вызывается функция из которой нет выхода, то этот путь становится для компилятора холодным
                                https://godbolt.org/z/bW78rv


                                Больше захардкоженных эвристик в gcc можно почитать здесь:
                                https://github.com/gcc-mirror/gcc/blob/master/gcc/predict.def


                                Что подтверждается: например, проверка указателя на 0 делает путь холодным https://godbolt.org/z/6PK7zs

                                  +1

                                  Это всё хорошо, но кажется про наличие или отсутствие фигурных скобочек всё же миф? Никакой метрики на тему в файле по ссылке нет для незнакомого с кодовой базой gcc взгляда.

                                    0
                                    И правда миф. В чатике pro.cxx его развенчивали
                              +9
                              Ну и C++20 не отстаёт:

                              Compute({.payload=payload, .timeout=1023});

                              Вы понимаете, что называя эмуляцию именованных параметров структурой «не отстает» и подавая ее как какое-то достижение C++20, вы закладываете мысль, что этот чудовищный костыль — в порядке вещей?

                              Сейчас это пойдет в массы, потом какой-нибудь гуру «паттерном» обзовет, а через 5 лет очередной study group сделает очередную выборку по github и придет к выводу, что всех все устраивает и нормальные именованные параметры можно и дальше не делать.
                                +1
                                Именованные параметры в C++ сделать невозможно без полного слома обратной совместимости (и с самим собой, из с C)
                                  +2
                                  Невозможно впихнуть в существующий синтаксис деклараций (точнее, возможно, но бессмысленно).
                                  Однако, это не «невозможно в принципе».
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  +2
                                  Смысл в мотивации писать читаемый код и в том, чтобы дать основы для этого.

                                  Примеры в статье смотрят не только C++ разработчики. Напихать C++ специфичного синтаксиса для экспертов, сделать метод шаблонным — это явно не те шаги, которые помогут усвоить материал.
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      Та информация, что вы хотите увидеть, находится в статье в секции «Приёмы»

                                      * «Сужать скоуп до минимально возможного» — очень хороший пойнт, как раз то, что я просил написать в комментариях и что я забыл в статье.
                                      * «Использовать общее решение» — да, но не уверен что это прям напрямую относится к читаемости кода и сложно назвать это простым приёмом.
                                      * «Не использовать макросы, там где можно спокойно обойтись функциями.» — скорее стоит сформулировать как «Не использовать не рекомендуемые практики», чтобы не привязываться к конкретному языку программирования. Но тогда совет получается весьма абстрактным
                                    +1
                                    Я писал несколько лет HFT системы и там подобные трюки(likely/unlikely) не давали даже микросекунды выигрыша.

                                    Правильно, они дают наносекунды выигрыша.


                                    Я писал HFT меньше года (и ещё несколько лет — всякий хайлоад или олдскульное машинное обучение на плюсах), и там этим вполне пользовались.

                                    • НЛО прилетело и опубликовало эту надпись здесь
                                        +1

                                        В самом предложении http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0479r0.html есть несколько тестов

                                          +2
                                          Реальный выигрыш на низком уровне есть в подгонке под кеши, лок фри структуры, статическая память вместо динамической, аллокации с учётом нума нодов, изоляция ядер, блокировка сигналов, кернел байпасы для tcp, интринсики, трюки с битовыми операциями, переделывание алгоритмов под конкретные данные. Я легко продемонстрирую увеличение производительности для любого из этих методов.

                                          Слишком высоко. Я на некоторых задачах наблюдал статистически значимый выигрыш в районе 0.5-1% от одного переупорядочивания инструкций.


                                          likely/unlikely — это примерно тоже самое что утверждать что операция a/2 выполняется дольше чем a<<1 (если будут тесты готов признать что неправ).

                                          Я даже не открывая таблицы Агнера Фога скажу, что сдвиг вправо быстрее целочисленного деления этак в 10-100 раз (НЯП, idiv для 64-битных регистров вообще имеет латентность и обратный трупут в районе 100 циклов, что не сравнится с этак единицей для shr).


                                          Другое дело, что современные компиляторы скомпилируют a / 2 в a >> 1, так что руками на С писать сдвиги в качестве оптимизации смысла не очень много.

                                      +1
                                      Недавно правил свой код двухлетней давности — плакал кровавыми слезами. И ведь старшие товарищи говорили мне как надо писать код, я а такой — «так работает же! И так сойдет».
                                      Зато хоть почувствовал и наглядно увидел повышение своего уровня за последние 2 года)
                                        +1
                                        Спасибо за статью! Как Вам удалось заменить 3 bool значения одним enum Options, если учесть, что возможны 8 их различных комбинаций, и значения по отдельности могут использоваться в разных функциях, вызываемых в Process()?
                                          0
                                          Если некоторые комбинации bool не имеют смысла или всегда кидают исключения — то значений в enum будет меньше.

                                          В реальном коде всё зависит от вашей задачи. Где-то удобнее будет объединить всё в 1 enum, где-то раскидать значения по 2-3 enum.
                                          +4
                                          Оказывается, что плохо работает не ваш код, а функция sample от другого разработчика, которую вы используете.


                                          Так где ошибка-то была? А то вы привели какой-то прямо каноничный пример:
                                          antoshkka:
                                          — Пришел в файл фиксить багу
                                          — (через 5 минут) Ой, кто это писал, что за ужас, поправим стиль.
                                          — (через 15 минут) мда, одним стилем не обойтись, надо капитально рефакторить
                                          — (через 30 минут) вот, теперь хорошо *горд собой и постит статью на хабр*
                                          Тимлид: «antoshkka, как там с тем багом, нашел проблему?»
                                          antoshkka: "*искренне удивлен* Каким багом? *мучительный мыслительный процесс* А, с тем багом! Ой...."

                                          Понятно, что хороший код лучше плохого, но не надо забывать о главной цели-то. И если код не такой уж и плохой (собственно, в исходном коде были косячки, ну магические числа, но ничего серьезного, что бы требовало рефакторинга ASAP, на мой взгляд), то может стоит сначала там тестов к нему написать, например, найти баг и пофиксить, и только потом за рефакторинг браться? Как говорится, Red-Green-Refactor.
                                            0
                                            Статья то не про баг, а про то как плохой стиль мешает их искать ;)
                                            +2
                                            Первая мысль, которая возникла при взгляде на код: кто пользуется ссылками на функции в 2021 году??? Это же все-таки С++, не С.
                                              0
                                              Можно, например, считать, что лучше использовать нативный механизм, чем хтоническое поделие из стандартной библиотеки (!) на не предназначенном для этого механизме.
                                              +1
                                              и сразу всё будет понятно.


                                              Это из комментария. Если читать статью вместе со всеми комментами, складывается довольно забавная картина. Вот автор взял чужой «плохой непонятный» текст и написал свой понятный ему в данный момент. Ему тут же накидали вариантов, понятных авторам этих вариантов. К чему это я? Давно живу, давно пишу. И прекрасно знаю одну вещь. Какие годы — через месяц-два, если «остыл» от задачи и переполз к другой, то старый текст начинает все больше и больше вызывать недоумение. Нет, разумеется, надо писать «хорошо». Константы в тексте — это, конечно, нонсенс. Но обо всем этом написано уже лет 40 назад все, что только можно. Рекомендации автора горячо поддерживаю. Но сильно опасаюсь, что их соблюдение не очень решит проблему на масштабе лет, ибо причина ее (проблемы), на мой непросвещенный взгляд, имеет более общий характер. Я допускаю, что есть выдающиеся люди, которые способны спустя, условно говоря, год снова, как ни в чем ни бывало, въехать в задачу и вновь почувствовать себя в ней как рыба в воде. Но полагаю, что большинство (и я в том числе) устроены иначе. «Холодный» старт всегда тяжелая штука. И свой собственный «правильно» написанный код спустя время воспринимается как изваянный полным придурком.
                                                +1

                                                Ну...


                                                1. оригинальный код конечно не идеален :) но прочесть его как-то проблем не вызвало, ну и пройти на описание функций в современных средах вопрос секунд. Да, за безименные параметры и отсутсвие описания надо бить по рукам, но тоже не конец жизни.
                                                2. переименование cb на on_data ничего не дало, кроме доп вопросов. cb стандартное сокращение и практически сразу становится понятно, что она делает. а когда обработка ошибки вызвает on_data тут же возникают вопросы почему on_data?
                                                3. А почему главную проблему (название функции Sample) не исправили? с нее начинается процесс понимания, что происхдит ;-)
                                                  –3

                                                  Мне кажется, что смешивать ассерты и кидание рантайм эксепции по одному условию — это демонстрация непонимания разницы между ассертами и рантайм ошибками. Ассерт нужно ставить там где нарушается контракт — баг — логическая ошибка. По другому, если после проваленного ассерта в коде не наблюдается УБ (в том числе вызывающем коде) то это не ассерт. Ещё по другому — рантайм ошибка/эксепция никогда не связанна с УБ, а наоборот штатная обработка внешних по отношению к программе невалидных данных.


                                                  Теперь смотрим ещё раз на ваш пример с ассертом — тип ошибки меняется из логической/баг в рантайм/штатная обработка/не баг в зависимости от дебажная это сборка или релизная. Я думаю теперь очевидно, что это какая-то ерунда, баг или есть или нет, в независимости от релиза/дебага.


                                                  ИНОГДА можно заменить ассерт на логическую эксепцию в релизе, но делать это нужно ОЧЕНЬ осторожно (привет не безопасный к эксепциям код) и только по очень специфическим причинам. Вообще в с/с++ отключение ассертов в релизе это преступление, за которое ...


                                                  Похоже из всех промышленных языков правильно из коробки обработка ошибок реализована только в расте.

                                                    +1
                                                    Теперь смотрим ещё раз на ваш пример с ассертом — тип ошибки меняется из логической/баг в рантайм/штатная обработка/не баг в зависимости от дебажная это сборка или релизная.

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

                                                      –1

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


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


                                                      Когда тип функции тотальная / не тотальная зависит от режима сборки это отличный способ отстреливать себе ноги по самые гланды.

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

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


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

                                                        А если у нас не тотальная функция, то UB возможно.


                                                        Когда тип функции тотальная / не тотальная зависит от режима сборки это отличный способ отстреливать себе ноги по самые гланды.

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

                                                          –1
                                                          Фунцкия, содержащая assert, (почти) никогда не является тотальной, даже если в релизной сборке assert вырезается.


                                                          Ну вот тут мы и расходимся в понимании. Мне кажется вы слишком буквально/педантично воспринимаете сигнатуру функции как единственный критерий (не)тотальности.

                                                          Смотрите
                                                          // тут все понятно
                                                          int total(int x)
                                                          {
                                                            return x;
                                                          }
                                                          // тут тоже все понятно
                                                          double partial(int x)
                                                          {
                                                            assert(x >= 0);
                                                            return sqrt(x);
                                                          }
                                                          optinal<double> total(int x)
                                                          {
                                                            if(x < 0) return nullopt;
                                                            return sqrt(x);
                                                          }
                                                          varaint<double, errc> total(int x) //either
                                                          {
                                                            if(x < 0) return errc{1};
                                                            return sqrt(x);
                                                          }
                                                          
                                                          // а вот тут уже не все понятно
                                                          double hz(int x)
                                                          {
                                                            assert(x >= 0);
                                                            0. if(x < 0) abort();
                                                            1. if(x < 0) throw logic_error("");
                                                            2. if(x < 0) throw runtime_error("");
                                                            return sqrt(x);
                                                          }
                                                          
                                                          void caller()
                                                          {
                                                             x = ...;
                                                             // в таком варианте частичная
                                                             hz(x);
                                                             try
                                                             {
                                                                hz(x);
                                                             }
                                                             catch(runtime_error)
                                                             {
                                                                // в таком варианте сюрприз hz это тотальная функция в релизе и частичная в дебаге
                                                                // а эксепция использована для эмуляции either
                                                                // это собственно моя главная претензия, такого бардака писать не надо
                                                             }
                                                             catch(logic_error)
                                                             {
                                                               // за такое надо сразу карать
                                                             }
                                                          }
                                                          
                                                          гдето далеко внизу колстека
                                                          void onEvent(event)
                                                          {
                                                              try
                                                              {
                                                                  procces(event); // где то в глубине вызывает caller
                                                              }
                                                              catch(exception)
                                                              {
                                                                 // это не всегда может закончиться нормально
                                                                 // но если очень осторожно и очень надо можно попробовать 
                                                                 // и хорошенько оттестировать в ручную
                                                              }
                                                          }
                                                          


                                                          Так вот ассерт + 0 = всегда ок, ассерт + 1 = опасно, но можно попробовать, ассерт + 2 = противоречие, остаться должен кто то один.

                                                          Если кидается runtime_error то подразумевается что ловить его можно и нужно, это просто иная форма variant/either. Если кидается logic_error то его точно нельзя ловить в непосредственном коллере, это просто сокрытие бага. И вот когда вы думаете ставить ассерт вы и должны решить это баг или нет. Если это не баг то это не ассерт, а если это баг то не кидайте рантайм эксепций или код ошибки.

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

                                                          Еще раз:
                                                          ассерт + код ошибки/рантйайм эксепция = противоречие
                                                          ассерт = очень опасно
                                                          ассерт + логическая эксепция = опасно
                                                          ассерт + аборт / невыключаемый ассерт = ок
                                                          код ошибки/рантйайм эксепция = ок
                                                            0

                                                            Если в функции double hz(int x) написано assert(x >= 0) — это означает, что поведение этой функции для x<0 не определено. Что именно там происходит (аборт, logic_error, runtime_error, SIGSEG, повреждение памяти) — совершенно не важно, эту функцию попросту нельзя вызывать для отрицательных параметров!


                                                            Любая корректная программа никогда не вызывает hz для отрицательного x. А потому нет никакой разницы как этот самый assert(x >= 0) раскрывается: в корректной программе это всё равно лишняя проверка.

                                                              –1

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


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

                                                                0

                                                                Да, вы правы. Те случаи, когда assert стоит "не по делу", я обсуждать не вижу смысла.


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

                                                                Не понимаю что это за случаи такие.


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

                                                                Не вижу причин чтобы UB произошло снаружи.

                                                                  –2
                                                                  Не понимаю что это за случаи такие.


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

                                                                  ASSERT(attempts > 0);
                                                                  if (attempts <= 0) throw NegativeAttempts();
                                                                  


                                                                  Не вижу причин чтобы UB произошло снаружи.


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

                                                                  Вот это техника «защитного» программирования нацелена на 1 причину, но при этом очень сильно портит жизнь когда настоящая причина срабатывания ассерта номер 2.

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

                                                                  На основе без преумеличения десятков примеров из опыта, я лично убежден (пока не предъявили контрпример), что «защитное» программирование это анти бест практис везде и всегда.
                                                              –1

                                                              Вообще, ИМХО, довольно странно рассуждать о тотальности в языке, подавляющий источник нетотальности в котором — система типов. Все эти извлечения корня спокойно становятся тотальными, если потребовать аргумент типа x >= 0. То есть, ну, разговор прикольный, но какая практическая польза?


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

                                                                0

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


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


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

                                                                  0
                                                                  А про пользу не тотальной функции вы уже сами ответили примером с корнем — эта функция фундаментально не тотальна для множества действительных

                                                                  А зачем вы её рассматриваете на множестве действительных?


                                                                  Я любую тотальную функцию f : A → B могу сделать нетотальной, определив f' : A U {A} → B и положив f' равной f на любом элементе A, и равной боттому на самом A. Но зачем?


                                                                  а за не имением ничего лучшего чем ассерты/контракты приходится спорить про то когда они уместны, а когда нет.

                                                                  Ну разве что.

                                                                    0

                                                                    Я с вами вообщем то полностью согласен — в теории. Я даже пробовал реализовывать что то наподобие PositiveInt и использовать его в как в вашем примере с корнем — и я считаю что это НАМНОГО лучше чем обратный подход когда в качестве результата используются нуллабл.


                                                                    На практике есть 2 фундаментальные проблемы с этим — мои коллеги не оценили сей подход от слова совсем. А вторая причина в том что с++ делает жизнь программиста невыносимой если нужно писать код на стыке подходов конверсии на конверсиях и конверсиями погоняет.


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


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

                                                      0
                                                      код, который не выбесит соседа

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

                                                      Важных моментов два:

                                                      1. Перед приходом в команду попросить показать код (хотя бы через скайп, хотя бы пару файлов) — визуально оценить, понравится ли вам работать с таким кодом. Если код, который пишет команда вам сразу не нравится — то и идти работать, наверное, не стоит.

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

                                                      P.S. Хороший код — это код в который не приходится заглядывать — который атомарно выполняет свою задачу и выполняет её хорошо и без ошибок :)
                                                        +3
                                                        Перед приходом в команду попросить показать код (хотя бы через скайп, хотя бы пару файлов) — визуально оценить, понравится ли вам работать с таким кодом. Если код, который пишет команда вам сразу не нравится — то и идти работать, наверное, не стоит.

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

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

                                                          Выбор в моих краях к сожалению не так чтобы большой — верхняя граница это — сойдет если не принюхиваться, но проблема не в коде, а в людях / процессах. Сойдет если не принюхиваться качество кода это такой баланс между кого организация может захантить за рыночную стоимость и минимально приемлемым качеством продукта. Так что из моей практики в целом код в любом многолетнем не маленьком коммерческом продукте всегда на грани фола — так дешевле. Поэтому я больше стараюсь выяснить про процессы чем смотреть на код.
                                                        +1
                                                        Автор прав вот буквально во всём. Одна только беда, потратив, например, час на понимание кода и его рефакторинг мы, фактически, поработали 25 минут за того мудака человека, который написал этот код плохо и ещё условно 25 минут для того человека, которому не придётся теперь читать и рефакторить этот код завтра, потому, что автор это уже сделал сегодня. На решение базовой проблемы, с которой автор начинал, ушло минут 10. А отчитываться вышестоящему начальству нужно будет за целый отработанный час. (Понятно, что я утрирую, за 1 час вряд ли кто-то будет отчитываться, но если вот так делать каждую задачу, то таких часов набежит много).

                                                        В итоге мы получаем ситуацию, когда делать правильно, как советует автор, не выгодно, если только ты не есть тем самым человеком, который писал это вчера или точно будет править это завтра. Ну или отвечаешь за весь релиз продукта и за все баги всех в команде.
                                                          +1
                                                          Если во время код-ревью сразу форсировать базовые правила по написанию читаемого кода — то и переписывать не придётся, и код сразу понятный будет комититься, и человек со временем начнёт писать читаемый код даже без ревью.
                                                            0
                                                            Я с вами согласен. Научите меня пожалуйста
                                                            форсировать базовые правила по написанию читаемого кода
                                                            да так, что бы в результате у вас конфликт не получился и исправление было реализовано. На моей практике, очень небольшое количество людей адекватно воспринимает критику и эти люди в наименьшей степени нуждаются в ней, большая часть уходит в глухую оборону при ЛЮБОМ способе донесения идей отличных от тех, что уже поселились в голове.
                                                              0
                                                              Мне как-то везло, встретил только 1 раз такого «упёртого». Со всеми остальными можно договориться, если доносить свою мысль без эмоционально, с объяснениями и показывать всё на собственном примере.
                                                          0
                                                          strong typedef и std::tuple становится читаемым и понятным
                                                            –2
                                                            была спокойно читаемая функция.
                                                            превратилась в абсолютно трудно-читаемую.

                                                              0
                                                              Как предлагаете улучшить читаемость?
                                                                +1
                                                                Придраться можно ко многому, но точно не к тому, что
                                                                if (result != -13)

                                                                process(result, true, false, true);

                                                                cb(data.second.first, data.second.second);

                                                                более читаемо, чем
                                                                if (new_descriptor == kBadDescriptor)

                                                                Process(Options::kSync, new_descriptor);

                                                                on_data(data.descriptor, data.payload);
                                                                  0
                                                                  Такие проблемы с читаемостью вылазиют, когда проект масштабируется и разрастается. Будь это единственная функция в проекте — и так сойдет. Как вы предлагаете мне запомнить все магические числа для всех возможных вариантов во всем коде, где, ну скажем, хотя бы 100к строк?
                                                                  0

                                                                  Чудеса, и как же автор такого кода прошел все круги вашего всесторонне-взвешенного интервью? Наверное, зато он сортирует массив как боженька, хотя и создавая 100500 переменных и магических чисел в процессе :)

                                                                    0

                                                                    Замечательная статья от Антона. От себя, я бы добавил еще правило:
                                                                    Вместо использования множественных флагов — используйте enum'чики или классы флагов на их основе. В Qt есть замечательный класс QFlags, который как раз для таких дел.


                                                                    Решается проблема:
                                                                    1) Аргументы внезапно получают имена
                                                                    2) Внезапно сокращается количество аргументов-флагов в прототипе функции

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

                                                                    Самое читаемое