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

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

Предварительная версия среды Visual Studio 2017 15.6 Preview 1 обеспечивает разработчиков инструментами для улучшения качества кода и очищения его от предупреждений.

Описание проблемы


Сметая шутки в сторону, следует отметить что не все предупреждения были созданы равными:

  • Одни отличаются точностью
  • Другие — эффективностью
  • Третьи — актуальностью
  • Четвертые — высокой скоростью обнаружения
  • Пятые — низким воздействием на имеющиеся базы кода

Увы, практически ни одно предупреждение не обладает сразу всеми пятью характеристиками. Любая диагностика сочетает в себе те или иные черты из этого списка, что порождает бесконечные дискуссии о том, о каких проблемах стоит уведомлять программиста, а о каких — нет. Само собой, в разных командах разработчиков используются разные критерии, а разработчики компиляторов пытаются создать расширенную классификацию диагностик, чтобы угодить всем этим критериям. Clang и GCC стараются проводить более тонкую дифференциацию предупреждений, объединяя их в семейства, а MSVC использует более грубое разделение на уровни важности предупреждений.

В проведенном нами опросе об улучшении диагностик (Diagnostics Improvements Survey) 15% из 270 респондентов заявили, что собирают код с ключами /Wall /WX, что указывает на их нулевую терпимость к любым предупреждениям. Еще 12% отметили, что запускают сборку с параметром /Wall, который включает в себя уровень /W4 плюс все выключенные по умолчанию предупреждения. Еще 30% собирают код на уровне /W4. Эти три непересекающиеся группы составляют 57% пользователей, которые подходят к качеству кода строже, чем предусмотрено настройками по умолчанию среды Visual Studio (уровень /W3) или самого компилятора (уровень /W1). Разделение предупреждений по уровням в известной степени произвольно и ни в коей мере не отражает наших собственных подходов. Например, команда, занимающаяся библиотеками MSVC, усердно вычищает код на уровне /W4.

Несмотря на отсутствие единого мнения насчет того, какие наборы предупреждений должен видеть разработчик, все согласны в том, что, какой бы набор ни был принят в конкретном проекте, из него в конце не должно остаться ни одного активного предупреждения: они все должны быть либо исправлены, либо подавлены. С одной стороны, при таком подходе каждое новое предупреждение служит порогом различения из пресловутого закона Вебера — Фехнера, а с другой стороны, в кросс-платформенном коде это необходимо, так как предупреждения, выдаваемые на такой код на одной платформе/компиляторе, часто превращаются в ошибки и более серьезные сбои на другой, о чем уже неоднократно сообщалось. Нулевую терпимость к предупреждениям легко привить в отношении внутреннего кода, но практически невозможно — в отношении внешнего кода сторонних библиотек, авторы которых могут использовать другой набор выдаваемых/исключаемых предупреждений. Требование, чтобы все библиотеки были очищены от всех известных предупреждений, и непрактично (из-за ложных срабатываний и отсутствия стандартной формы записи для их подавления), и недостижимо (так как множество предупреждений непрерывно расширяется). Второе объясняется тем, что экосистемы компиляторов и библиотек развиваются совместно, и улучшения в одних вызывают необходимость в улучшениях — а значит, и необходимость держать заданный темп — в других. В результате разработчикам приходится иметь дело то с компиляторами, отставшими от библиотек, то с библиотеками, отставшими от компиляторов, — причем ни те, ни другие им не подконтрольны. В таких обстоятельствах программисты (при условии, что они пишут на живых и активных языках вроде C++) хотят сами определять, какие предупреждения они хотели бы видеть в коде, который им неподконтрольный.

Предлагаемое решение


Мы вводим новую группу параметров компиляции /external:*, которая работает с «внешними» заголовочными файлами. Мы предпочли название "внешние заголовочные файлы" названию "системные заголовочные файлы", принятому в других компиляторах, поскольку оно лучше отражает все разнообразие существующих сторонних библиотек. Кроме того, С++ стандарт уже апеллирует к внешним заголовочным файлам в разделе [lex.header], так что наш выбор вполне естественен. Мы объединили новые ключи в группу, вместо того чтобы описывать их по отдельности, потому что так пользователям будет легче освоить их: полный синтаксис новых ключей можно предугадать по аналогии с уже известными ключами. На данный момент группа состоит из 5 параметров, разделенных на две категории (см. соответствующие разделы ниже):

Параметры, задающие набор внешних заголовочных файлов


  • /external:I <path>
  • /external:anglebrackets
  • /external:env:<var>

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


  • /external:W<n>
  • /external:templates-

Вторая группа может быть в дальнейшем дополнена такими параметрами, как /external:w, /external:Wall, /external:Wv:<version>, /external:WX[-], /external:w<n><warning>, /external:wd<warning>, /external:we<warning>, /external:wo<warning> и т.д. Их можно будет использовать, как аналоги уровней предупреждений или любых других стандартных ключей для внешних (в отличие от пользовательских) заголовочных файлов. Обратите внимание, что, поскольку это экспериментальная функция, для ее включения придется добавлять параметр /experimental:external, пока мы не отладим ее до конца. Посмотрим, что делают новые ключи.

Внешние заголовочные файлы


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

  • /external:I <path> — идейный аналог -isystem, или просто -i (в нижнем регистре), в компиляторах GCC, Clang и EDG, с помощью которого задается директория с внешними заголовочными файлами. Все рекурсивные поддиректории в пути также считаются внешними, но к списку директорий, по которым производится поиск включаемых файлов, добавляется только сам путь;
  • /external:env:<var> — задает имя переменной окружения, в которой хранится список директорий с внешними заголовочными файлами, перечисленных через точку с запятой. Этот способ полезен при использовании сборочных систем, полагающихся на такие переменные окружения, как INCLUDE и CAExcludePath, где задается список внешних включаемых файлов и файлов, которые не должны проверяться ключом /analyze, соответственно. Пользователь может просто написать /external:env:INCLUDE и /external:env:CAExcludePath и не передавать длинный список директорий с параметром /external:I;
  • /external:anglebrackets — позволяет интерпретировать все заголовочные файлы, включенные с помощью команды #include <> (в противоположность #include ""), как внешние;
  • #pragma system_header — внедряемая метка заголовочных файлов — позволяет авторам библиотек помечать те или иные заголовочные файлы как внешние.

Уровни предупреждений для внешних заголовочных файлов


Параметр /external:W<n> позволяет пользователю задать уровень предупреждений по умолчанию для внешних заголовочных файлов. Такие включения мы оборачиваем в аналог конструкции:

#pragma warning (push, n)
// уровень предупреждений n в этом коде
#pragma warning (pop)

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

Пример:


Внешний заголовочный файл: some_lib_dir/some_hdr.hpp

template <typename T>
struct some_struct
{
  static const T value = -7; // W4: warning C4245: 'initializing':
                             // conversion from 'int' to
                             // 'unsigned int', signed/unsigned
                             // mismatch
};

Пользовательский код: my_prog.cpp

#include "some_hdr.hpp"
int main()
{
  return some_struct<unsigned int>().value;
}

Если скомпилировать этот код следующим образом:

cl.exe /I some_lib_dir /W4 my_prog.cpp

на заголовочный файл будет выдано предупреждение C4245 4-го уровня, упомянутое в комментарии. Компиляция с параметрами:

cl.exe /experimental:external /external:W0 /I some_lib_dir /W4 my_prog.cpp

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

cl.exe /experimental:external /external:I some_lib_dir /W4 my_prog.cpp

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

cl.exe /experimental:external /external:I some_lib_dir /external:W0 /W4 my_prog.cpp

Эта команда позволит избавиться от всех предупреждений на файл some_hdr.hpp, оставив только предупреждения на файл my_prog.cpp.

Предупреждения, затрагивающие и внутренний, и внешний код


Было бы замечательно, если бы можно было просто задать уровень предупреждений для внешних заголовочных файлов, однако так мы рискуем отсеять некоторые сообщения, актуальные также и для внутренних, пользовательских файлов. Если использовать одни только pragma-директивы push/pop в связке с include, могут пропасть многие полезные предупреждения при инстанцировании шаблонов в пользовательском коде. Такие предупреждения могут указывать на наличие проблемы именно на стороне пользователя, причем проявляется она при подстановке в шаблон лишь некоторых типов (например, когда забыли применить преобразование типа из <type_traits>, убирающее const или &), и тогда об этой проблеме стоит сообщить. До этой версии уровень предупреждений, действующий в момент выдачи сообщения, определялся исключительно на основе лексического анализа, тогда как проблемный участок может находиться в другой области видимости. По-видимому, есть смысл сравнивать уровни предупреждений в точках инстанцирований шаблонов, чтобы определять, какие предупреждения должны выдаваться, а какие — нет.

Чтобы случайно не заглушить предупреждения в шаблонах, определения которых находятся во внешних заголовочных файлах, мы разрешили пользователям исключать шаблоны из описанной выше упрощенной логики — это можно сделать, передав параметр /external:templates- вместе с /external:W<n>. При этом мы смотрим не только на текущий уровень предупреждений в точке, где содержится определение шаблона и выдается предупреждение, но и на уровни предупреждений во всех точках в последовательности инстанцирований шаблона. Наши уровни предупреждений образуют решетку по отношению ко всему множеству сообщений всех уровней (впрочем, она не идеальна, поскольку иногда мы выдаем предупреждения на нескольких уровнях сразу). Надмножество, определяющее предупреждения, что должны быть допущены в заданной программной точке в отношении этой решетки, получалось бы объединением сообщений, разрешенных в каждой программной точке через цепочку инстанцирований. Именно для этого и служит ключ /external:template-, позволяя отображать предупреждения на шаблоны, хранящиеся во внешних заголовочных файлах и реализуемые в пользовательском (т.е. внутреннем) коде.

cl.exe /experimental:external /external:I some_lib_dir /external:W0 /external:templates- /W4 my_prog.cpp

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

Подавление и принудительное включение предупреждений


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

  • /wdNNNN, /w1NNNN, /weNNNN, /Wv:XX.YY.ZZZZ etc.
  • #pragma warning( disable: 4507 34; once: 4385; error: 4164 )
  • #pragma warning( push[ ,n ] ) / #pragma warning( pop )

Кроме того, при использовании /external:templates- можно подавлять предупреждение в точке инстанцирования. В примере, рассмотренном ранее, пользователь может явно подавить предупреждение, выданное из-за ключа /external:templates-:

int main()
{
#pragma warning( suppress : 4245)
  return some_struct<unsigned int>().value;
}

Работающие по другую сторону авторы библиотек могут использовать эти же самые механизмы для принудительного включения некоторых или вообще всех предупреждений определенного уровня, если они считают, что эти предупреждения достаточно критичны и не должны подавляться ключом /external:W<n>.

Пример:


Внешний заголовочный файл: some_lib_dir/some_hdr.hpp

#pragma warning( push, 4 )
#pragma warning( error : 4245 )
template <typename T>
struct some_struct
{
  static const T value = -7; // W4: warning C4245: 'initializing':
                             // conversion from 'int' to
                             // 'unsigned int', signed/unsigned
                             // mismatch
};
#pragma warning( pop )

Изменив библиотечный заголовочный файл, как показано выше, автор библиотеки может быть уверен, что этот файл будет проверяться с уровнем 4 независимо от того, какой уровень указал пользователь в параметре /external:W<n>, — компилятор все равно выдаст все предупреждения уровня 4 и выше. Более того, как показано там же, можно принудительно настроить то или иное предупреждение так, что оно всегда будет считаться ошибкой, выключаться, подавляться или выдаваться единожды для данного заголовочного файла — и, опять же, пользователь никак не сможет обойти эту настройку.

Ограничения


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

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

Мы пока не можем сказать, как эта функция будет работать с предупреждениями SDL, но мы будем поддерживать контакт с командой SDL и сообщим вам, как только узнаем.

Заключение


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

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

Позволяя разработчикам скрывать предупреждения на сторонние библиотеки, мы поощряем их уделять больше внимания собственному коду, улучшать его, а то и полностью очищать от предупреждений на максимально возможном для них уровне. Авторы сторонних библиотек — такие же разработчики, еще одно звено в этой цепи, а значит, давая им возможность не отвлекаться на зависимости в стороннем коде, мы так же поощряем их тщательнее следить за качеством собственного кода и добиваться компилируемости на максимально возможном для них уровне предупреждений. И так далее. Почему это важно? Дело в том, что при нынешнем устройстве процесса разработки ПО число предупреждений растет лавинообразно по мере продвижения по цепочке зависимостей, и чем дальше вы в этой цепочке, тем труднее становится справляться с предупреждениями; разработчики «ломаются» под их тяжестью и прекращают любые попытки исправить ситуацию. Когда же есть возможность отделить свой код от чужого, каждый разработчик в цепочке получает в свое распоряжение средства для остановки (блокирования последствий) этой лавины предупреждений; у него появляется стимул к тому, чтобы свести к минимуму ее влияние на свой участок, а тем самым и на всю цепочку в целом. Это, конечно, умозрительное заключение, но мы считаем, что этот эффект не менее вероятен, чем негативное воздействие на библиотеки.

В завершение мы приглашаем вас испытать новый функционал самостоятельно и поделиться своими впечатлениями с нами. Убедительная просьба: рассказывайте не только о том, что вам понравилось, но и о том, что не понравилось, иначе активное меньшинство решит за вас. Новый механизм доступен в предварительной версии среды Visual Studio 15.6 Preview 1. Как обычно, связаться с нами можно, оставив комментарий внизу либо написав нам электронное письмо на адрес visualcpp@microsoft.com; отзыв можно отправить с помощью команды меню Help -> Report A Problem in the product («Сообщить о проблеме») или оставить в сообществе разработчиков. Подписывайтесь на наши страницы в Twitter (@VisualC) и Facebook (msftvisualcpp).

P.S. Отдельное спасибо Роберту Шумахеру — это он указал на сходство нашей теории с теорией разбитых окон!

Примечания переводчика


  1. Статья публикуется с согласия автора. Оригинал статьи опубликован в Visual C++ Team Blog на английском языке: Broken Warnings Theory.
  2. Профиль автора на сайте GitHub: solodon4.
  3. Обсуждение статьи на сайте Reddit.

Комментарии (7)


  1. VolCh
    28.01.2018 23:52

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


    1. mayorovp
      29.01.2018 06:10

      А чем переписывание для компилятора принципиально отличается от подавления?


      1. VolCh
        29.01.2018 11:37

        Изменение непосредственно кода против изменения каких-то метаданных о коде.


    1. sergey_shambir
      29.01.2018 08:24

      Есть вариант "подавить, сохранив читаемость", то есть подавить без директив #pragma. В известных мне компиляторах конкретно это предупреждение подавляется с помощью двойных скобок: if ((x = 5)) ...
      Без дополнительных скобок: https://wandbox.org/permlink/kHKy1aBtQR5HIEqQ
      С дополнительными скобками: https://wandbox.org/permlink/kyKM2SmKliw1L8qB
      Предупреждения unused parameter/variable гасятся с помощью инструкции (void)&x;.


      1. VolCh
        29.01.2018 11:39

        Этот вариант я отношу к "переписать код".


        1. mayorovp
          29.01.2018 12:47

          А где дополнительные переменные и строки кода?


          1. VolCh
            29.01.2018 12:49

            "пускай это и потребует" означает, что может и не потребовать.