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

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

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

Не игнорируйте предупреждения компилятора


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

Но будьте уверены, что одно из нескольких тысяч предупреждений компилятора действительно имеет смысл. Иногда мы пишем код, который компилируется, но что-то работает не так, как мы предполагали. Так как же, во имя Хабра, определить, какое именно предупреждение указывает на реальную ошибку? Как отличить его от сотен других, при которых код действителен? Неужели нужно тратить столько времени на то, чтобы прочитать их все? Это практически невозможно.
Примите политику «без предупреждений»

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

Вы можете сказать, что если предупреждений компилятора всего несколько — вы вполне сможете с этим жить. Тут вот какая штука: вы, наверное, проверяете свой код относительно часто (по крайней мере, мы на это надеемся). Это значит, что вам приходится часто компилировать, а это влечет за собой предупреждения компилятора. Вы станете обращать на них внимание. Возможно, вы все еще будете уделять им внимание, если получите 6 предупреждений вместо 5; но заметите ли вы 11-е? 20-е? 52-е?
Избавиться от предупреждений — единственный приемлемый и безопасный для психики вариант.

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


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

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

image
Для Clang это выглядит так:

image
Второй случай, который лежит в основе этого предупреждения: иногда мы пишем a = b, когда мы имели в виду a == b.

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

Первый указатель просто говорит нам, как исправить предупреждение, если задание на самом деле так и было задумано. У GCC есть такое же предупреждение и предложение исправить, но без предоставления альтернативы:

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

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

С точки зрения чистого кода, мы должны разделить присваивание и условие. Гораздо удобнее иметь по одной строке для каждой задачи, то есть, по сути, применять Принцип Единой Ответственности на построчном базисе:

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

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

Флаги компилятора


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

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

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

Стандартные флаги для большинства предупреждений — это Wall, Wpedantic, Wextra (многие флаги компилятора, касающиеся предупреждений, начинаются с W).
Если вы только начинаете применять политику «без предупреждений» для своего проекта, вы, скорее всего, получаете сотни или даже тысячи предупреждений, если они все включены. Начните с более низкого уровня предупреждений. Зафиксируйте наиболее жесткие предупреждения и постепенно наберите свой уровень предупреждений.

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

Чтобы избежать такой траты времени, вы можете усилить политику «без предупреждений», переключив предупреждения в ошибки. Таким образом, ошибки будет сложно проигнорировать — иначе билд просто провалится. Так можно поступить с отдельными предупреждениями, а также со всеми предупреждениями одновременно. Установите соответствующий флаг -Werror для Clang и GCC и /WX для MSVC.

Директивы Pragma


Компиляторы часто используют особые директивы #pragma, позволяющие включить или отключить конкретные предупреждения кода. Эти директивы следует рассматривать как временное решение, потому что у них есть свои недостатки:

  • Отключение предупреждений с помощью #pragma заглушает компилятор для всего оставшегося юнита. Если вы хотите отключить это предупреждение только однократно, обязательно включите его прямо после строки кода с вопросом. Включение #pragma для файла заголовка и решение не включать предупреждения снова заглушит компилятор для каждого источника, имеющего заголовок, а также для каждого заголовка после #pragma
  • Директивы #pragma для предупреждений не портативны. Идентификаторы для каждого отдельного предупреждения варьируются для разных компиляторов, также как и формат #pragma. Иногда компиляторы выдают предупреждения о неизвестных #pragma — и вы определенно не захотите писать прописывать в GCC предупреждении #pragma, что ему стоит игнорировать эти MSVC предупреждения. Заключить их в #ifdefs явно не самое лучшее решение.

Бывают случаи, когда вам не подходит использование #pragma.

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

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

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


Вы можете настроить компилятор, сообщив ему, какие предупреждения вам интересны, а какие — нет; используйте для этого аргументы командной строки или, если потребуется, директивы #pragmas. Старайтесь быть максимально строгими, не добавляйте слишком много исключений. Это означает осторожное использование #pragmas и отклонений от ваших стандартных аргументов командной строки.

P. S. Иногда ни один из перечисленных способов не является реалистичным решением. Тогда просто используйте для компилятора команду shut up.
Поделиться с друзьями
-->

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


  1. Ivan_83
    18.05.2016 10:49

    Для clang есть ещё дополнительно: -Weverything
    но я после него некоторые варнинги отключаю через -Wno-… потому как это уже борьба с ветряными мельницами начинается:
    -Wno-gnu-zero-variadic-macro-arguments;-Wno-variadic-macros;-Wno-padded;-Wno-packed;-Wno-unused-macros;-Wno-reserved-id-macro;-Wno-date-time


  1. Lau
    18.05.2016 13:50

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


    1. Ivan_83
      18.05.2016 14:20

      Ошибки в компиляторах — отдельная больная тема.
      Всегда считал что компиляторы пишут умные люди и тестируют и ошибок там нет, но за прошедший месяц нашёл там пару косяков: один приводил к тому, что инстрикт возвращал неверное значение, а другой к падению производительности в 5-10 раз при использовании другого инстрикта.


      1. Andrey2008
        18.05.2016 17:15

        Ну прям уж проблема… Во всём виноват компилятор. :)


        1. Ivan_83
          19.05.2016 00:08

          Если бы всё было так просто.
          https://llvm.org/bugs/show_bug.cgi?id=27594
          https://llvm.org/bugs/show_bug.cgi?id=27737


          1. laphroaig
            19.05.2016 02:52

            Ошибки могут быть везде, но любую ошибку можно обойти множеством способов


  1. elmm
    18.05.2016 15:08

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


    1. DarkEld3r
      18.05.2016 16:18
      +1

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


      1. monah_tuk
        18.05.2016 17:48

        Да не некоторые, а, пожалуй, почти все. Только делается это не стандартно. Ну и чуточку более корректно: они могут отключать предупреждения для участков кода, соответственно заголовочник библиотеки подключается, в наиболее удобном варианте, через враппер. Например, мой libusb_wrapper.h для gcc:


        #pragma once
        
        #pragma GCC diagnostic push
        #pragma GCC diagnostic ignored "-Wpedantic"
        #include <libusb.h>
        #pragma GCC diagnostic pop


        1. encyclopedist
          19.05.2016 23:50

          Для GCC и Clang достаточно указать директорию с помощью -isystem <DIR> вместо -I <DIR>


  1. Andrey2008
    18.05.2016 17:12
    -1

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

    А быть может, достаточно просто использовать возможности компилятора Visual C++ (или FxCop)? Нет, не достаточно: Демонстрация возможностей анализатора PVS-Studio на примере открытых проектов компании Microsoft.


  1. monah_tuk
    18.05.2016 17:53

    Тема — прописная истина. Что не мешает ей быть чертовски актуальной. С другой стороны, в коде без ворнингов можно использовать следующий трюк: все знают, что временами код обрастает всякими TODO/FIXME и так далее, и живут они потом там вечно. А если вместо TODO вставить:


    #warning TODO: remove this ugly comment

    то при отсутствии других ворнингов это будет назойливо мозолить глаза и шансов на выпил причин данного TODO будет больше.


  1. laphroaig
    19.05.2016 02:37

    Как тимлид решил проблему радикально: -Wall -pedantic -Wextra -Werror
    Здесь ключевое слово -Werror. А если…


  1. 4144
    28.05.2016 16:19

    Для кросс компиляторного использования #pragma, есть такое ключевое слово _Pragma.
    Это аналог #pragma, но его можно использовать в макросах.
    Т.е. можно объявить свою прагму, которая будет на пример работать только для clang, а gcc её будет игнорировать.
    Примерно так:
    #ifdef __clang__
    #define PRAGMACLANG(str) _Pragma(#str)
    #else
    #define PRAGMACLANG(str)
    #endif

    И теперь в коде для заглушения какого-то предупреждения clang, можно написать:
    PRAGMACLANG(clang diagnostic push)
    PRAGMACLANG(clang diagnostic ignored "-Wsomething"
    … здесь какой-то код…
    PRAGMACLANG(clang diagnostic pop)

    Также можно учитывать версию компиляторов, включенный стандарт C/C++ кода и т.д.