вторник, 13 апреля 2010 г.

Больше предупреждений, хороший и разных

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

Есть предупреждения, применимые только для C++ (например: -Weffc++, -Woverloaded-virtual, -Wold-style-cast). Я не пробовал включать их для си, могу предположить, что последует ругань и правильно. А поскольку для C они не имеют смысла - в данном контексте они не интересны.

Для обоих языков в первом приближении я отобрал три варнинга: -Wsign-conversion -Wconversion -pedantic. Остановимся на них поподробнее.

Начать стоит с того, что в си enum - всегда int.
enum {
 TEST1 = 0x100000000ULL,
 TEST2 = 0xffffffffU,
 TEST3 = 0x7fffffff
};
$ gcc -c -std=c99 -pedantic test.c
test.c:2: предупреждение: в ISO C значения перечислимого типа ограничены диапазоном типа ‘int’
test.c:3: предупреждение: в ISO C значения перечислимого типа ограничены диапазоном типа ‘int’
Причем это справедливо и для amd64, в этом легко убедиться например так:
BOOST_STATIC_ASSERT(sizeof(int) == 4);
В то же время в C++ с enum не возникает никаких проблем. В C++ тип enum есть собственно enum, даже знаковость которого определяется позже. Единственный возникающий в данной ситуации момент - стандарт C++ 99 года не любит определений ULL, считая их C99 long long integer constant, Для C++ даже U писать не обязательно, главное, чтобы значение могло уместиться в long, то есть быть не больше 32-64 бит в зависимости от типа платформы.

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

Значение sizeof(TEST1) в С++ варьируется в зависимости от наполнения enum от 4 до 8.

Другой интересный момент, справедливый для обоих языков:
const unsigned int t1 = TEST1;
const unsigned int t2 = 0;
const unsigned int t3 = argc > 1 ? TEST1 : 0;
$ gcc -m64 -o test -std=c99 -Wsign-conversion test.c
test.c:3: предупреждение: conversion to ‘unsigned int’ from ‘int’ may change the sign of the result
Ошибка возникает в третьей строке фрагмента, и лечится заменой 0, на 0U... Хотя тот же 0 во второй строке не вызывает проблем.

И напоследок - ошибочная ситуация в gcc:
При попытке присвоить битовому полю значение какой-то переменной практически всегда происходит ошибка, и обойти ее практически не представляется возможным.
unsigned int x = 0x7f;
struct {
 unsigned int a:7;
} t = { .a = x };
$ gcc -std=c99 -Wconversion test.c
test.c:4: предупреждение: conversion to ‘unsigned char:7’ from ‘unsigned int’ may alter its value
Не существует способа привести переменную к битовому полю.

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

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

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

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