C++ is statically typed. Programmers inform their compiler about the types they are using. In return the compiler warns them of any type abuse it detects. Static typing doesn't cure all known defects, of course. On occasion, a defect which appears type-related can slip through the type system undetected. This article tells the true story of two such defects. It discusses how they might have been avoided and concludes with some more general advice.
В статье автор рассматривает два реальных бага, которые он нашел и исправил, и которые не появились бы, если бы при разработке уделялось внимание... как бы это сказать... потенциальной возможности ошибиться с типами аргументов.
Warning: статья на английском.
Disclaimer: мне понравилось, хотя некоторые описанные в вещи отнюдь не новые.
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
eao197 wrote: > В статье автор рассматривает два реальных бага, которые он нашел и > исправил, и которые не появились бы, если бы при разработке уделялось > внимание... как бы это сказать... потенциальной возможности ошибиться с > типами аргументов.
Второй баг можно исправить с помощью Boost.Parameters:
C>Причем Boost.Parameters написан так, что дает нулевой оверхед.
Если бы ты дочитал до четвертой страницы, то увидел бы:
A more radical approach would be to enlist the Boost Parameters library [8], which uses some astonishing metaprogramming techniques to provide C++ with keyword arguments, allowing us to call our new function as follows (for example):
boostTextRender(text = "Built in Type Safety?",
bold = true,
region = full_screen);
or, equivalently:
boostTextRender(region = full_screen,
bold = true,
text = "Built in Type Safety?");
Ох уж эти нетерпеливые писатели/читатели
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
Здравствуйте, eao197, Вы писали: E>Еще одна статья из The Artima C++ Source: Built-in Type Safety?.
Вкратце опишу первую ошибку ("defect one") для тех, кто не читал статью. Есть целое число от 0 до 100 ("signal quality"). Поначалу оно хранилось в переменной типа int:
/**
* Typedef used to represent signal quality as a percentage.
* 100 represents perfect signal, 0 no signal.
*/typedef int Signal;
Потом кто-то решил заменить int на unsigned ("someone decided that an unsigned was more appropriate for a value in the range [0, 100]"):
/**
* Typedef used to represent signal quality as a percentage.
* 100 represents perfect signal, 0 no signal.
*/typedef unsigned Signal;
Ну и получили ошибку: "So, 10u — 20 is a very big unsigned number." Автор (Thomas Guest) делает из этого четрые вывода ("lessons learned"), но не делает главного: избегайте беззнаковой арифметики.
Здравствуйте, Пётр Седов, Вы писали:
ПС>Потом кто-то решил заменить int на unsigned ("someone decided that an unsigned was more appropriate for a value in the range [0, 100]"): ПС>
ПС>
ПС> /**
ПС> * Typedef used to represent signal quality as a percentage.
ПС> * 100 represents perfect signal, 0 no signal.
ПС> */
ПС> typedef unsigned Signal;
ПС>
ПС>Ну и получили ошибку: "So, 10u — 20 is a very big unsigned number." Автор (Thomas Guest) делает из этого четрые вывода ("lessons learned"), но не делает главного: избегайте беззнаковой арифметики.
Осталось только доказать, что на момент написания кода с арифметикой тип Signal уже был беззнаковым. Скорее всего, что на тот момент тип Signal был знаковым. А тот, кто изменял тип Singal, скорее всего, даже не знал, какие арифметические операции над ним производятся. Обычная ситуация.
Так что вывод "избегайте беззнаковой арифметики" в данном случае вполне можно довести до логического завершения: избегайте программирования. Вообще. Ибо крайне подверженное ошибкам занятие.
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
Да, вещи не новые. Но, во-первых, всё так сказать разложено по полочкам. Во-вторых, хороший пример, который можно привести коллеге, который решил использовать беззнаковую арифметику. Потомучто не всегда есть что возразить на аргумент "ну в этой же переменной не могут содержаться отрицательные числа".
В общем, мне понтравилось.
Специально попробовал скомпилировать пример:
unsigned u = 0;
int i = 0;
u = i;
i = u;
msvc80sp1 уровень предупреждений 4 — ни одного варнинга. Хотя оба из присваиваний могут вызвать проблемы...
Недавно сам допустил аналогичную ошибку. Одна переменная при передаче по определённому протоколу имеет тип unsigned char (byte). А в программе она была описана как long. Ну я и заменил long на byte. Типа думаю, если кто будет присваивать этой переменной, например, long, то сразу будет варнинг.
Но подкололся я на другом. С этой переменной не производилось никаких математических операций. И варнинг при присваивании long действительно появился... Но оказалось в одной подсистеме программы этой переменной присваивали -1, что означало "не задано"... Результат понятен.
R> unsigned u = 0;
R> int i = 0;
R> u = i;
R> i = u;
// А вот это приведет к предупреждениюif( i < u) ...
// А вот это нетif( i == u) ...
R>
Это так, информация к размышлению...
With best regards
Pavel Dvorkin
Re[3]: Беззнаковая арифметика
От:
Аноним
Дата:
15.10.06 17:36
Оценка:
Здравствуйте, eao197, Вы писали:
E>Осталось только доказать, что на момент написания кода с арифметикой тип Signal уже был беззнаковым. Скорее всего, что на тот момент тип Signal был знаковым.
Да, во время написания кода было Signal = int. Цитата из раздела "History":
Rewinding the header file shows that once upon a time we had:
/**
* Typedef used to represent signal quality as a percentage.
* 100 represents perfect signal, 0 no signal.
*/typedef int Signal;
The code worked just fine back then.
E>А тот, кто изменял тип Singal, скорее всего, даже не знал, какие арифметические операции над ним производятся. Обычная ситуация.
Если бы этот программист знал совет "избегайте беззнаковой арифметики" и следовал ему, то он не поменял бы int на unsigned. И ошибка не возникла бы.
Это большой соблазн: "раз переменная содержит только неотрицательные значения, то пусть будет unsigned". В эту ловушку уже попалось много программистов (включая меня) и, видимо, ещё много попадётся.
E>Так что вывод "избегайте беззнаковой арифметики" в данном случае вполне можно довести до логического завершения: избегайте программирования. Вообще. Ибо крайне подверженное ошибкам занятие.
Волков бояться — в лес не ходить. Но если уж идти в лес (хочется кушать), то разумно максимально обезопасить себя от волков (например, взять оружие или разжечь костёр). C/C++ — очень свободные языки. Чтобы обезопасить себя от ошибок, нужна само-дисциплина. Многие советы/правила программирования на C/C++ — это запреты: "не делайте так-то", "не используйте то-то". Например, многие команды стараются избегать голых указателей в основном коде. Используют обёртки и умные указатели. Так что "избегайте беззнаковой арифметики" не влечёт "избегайте программирования".
Аноним wrote: > Это большой соблазн: "раз переменная содержит только неотрицательные > значения, то пусть будет unsigned". В эту ловушку уже попалось много > программистов (включая меня) и, видимо, ещё много попадётся.
Есть случаи, когда без unsigned очень плохо. Например, при работе с флагами.
Здравствуйте, Cyberax, Вы писали: C>Аноним wrote: >> Это большой соблазн: "раз переменная содержит только неотрицательные значения, то пусть будет unsigned". В эту ловушку уже попалось много программистов (включая меня) и, видимо, ещё много попадётся. C>Есть случаи, когда без unsigned очень плохо. Например, при работе с флагами.
Да, но обратите внимание на тему: "Беззнаковая арифметика". Речь не о том, что unsigned опасен, а о том, что беззнаковая арифметика опасна. Если переменная типа unsigned интерпретируется не как целое число, а как последовательность битов, то беззнаковой арифметики нет.
Кстати, я не люблю флаги. Во-первых, не type-safe. Я видел на просторах RSDN что-то вроде:
WS_OVERLAPPEDWINDOW | CS_OWNDC
Это как "2 метра + 3 секунды". Во-вторых, в отладчике тяжело узнать состояния отдельных флагов, потому что отладчик интерпретирует переменную как целое число.
Здравствуйте, Аноним, Вы писали:
А>Да, во время написания кода было Signal = int. Цитата из раздела "History":
E>>А тот, кто изменял тип Singal, скорее всего, даже не знал, какие арифметические операции над ним производятся. Обычная ситуация.
Не то выделили. Нужно сделать упор на: какие арифметические операции над ним производятся.
Вполне возможно, что автору изменений типа Signal был виден код, в котором над Signal использовалось только сложение.
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
Здравствуйте, Пётр Седов, Вы писали:
ПС>Кстати, я не люблю флаги. Во-первых, не type-safe. Я видел на просторах RSDN что-то вроде: ПС>
ПС>WS_OVERLAPPEDWINDOW | CS_OWNDC
ПС>
ПС>Это как "2 метра + 3 секунды".
+1
ПС>Во-вторых, в отладчике тяжело узнать состояния отдельных флагов, потому что отладчик интерпретирует переменную как целое число.
-1
Hex юзать.
Да и вообще эта type safety когда помогает, а когда и нет... Если есть 2 разных std::list<int>, то хочется, чтобы splice было бы можно, а итераторы сравнивать было бы запрещено. Т. е., они должны быть одного типа, и в то же время разного.
Пётр Седов wrote: > Да, но обратите внимание на тему: "Беззнаковая арифметика". Речь не о > том, что unsigned опасен, а о том, что беззнаковая арифметика опасна. > Если переменная типа unsigned интерпретируется не как целое число, а как > последовательность битов, то беззнаковой арифметики нет.
Еще один пример где со знаковой арифметикой неудобно — это работа с
изображениями.
Здравствуйте, Cyberax, Вы писали: C>Пётр Седов wrote: >> Да, но обратите внимание на тему: "Беззнаковая арифметика". Речь не о том, что unsigned опасен, а о том, что беззнаковая арифметика опасна. Если переменная типа unsigned интерпретируется не как целое число, а как последовательность битов, то беззнаковой арифметики нет. C>Еще один пример где со знаковой арифметикой неудобно — это работа с изображениями.
Изображения хранятся как массивы байтов (байт = unsigned char), чтобы экономить память. Но беззнаковой арифметики опять нет, так как в выражениях unsigned char продвигается (promote) до int (а не unsigned). Страуструп пишет в книге "Язык программирования C++" (третье издание):
В.6. Неявное преобразование типов
... В.6.1. Продвижения
Неявные преобразования, сохраняющие значение, обычно называют продвижениями. Перед тем, как выполнить арифметическую операцию, используется интегральное продвижение — для того, чтобы создать переменные типа int из переменных более "коротких" целых типов.
...
Интегральные продвижения таковы:
char, signed char, unsigned char, short int или unsigned short int преобразуются в int, если int может представить все значения исходных типов; в противном случае они преобразуются в unsigned int.
...
Продвижения используются как часть обычных арифметических преобразований (§ В.6.3).
... В.6.3. Обычные арифметические преобразования
Эти преобразования выполяются над операндами бинарного оператора, чтобы привести их к общему типу, который потом используется как тип результата:
...
Например, есть переменная типа unsigned char:
unsigned char n = 10;
Выражение 'n — 20' имеет тип int и равно -10, а не "very big unsigned number":
ostringstream s;
s << n - 20;
assert(s.str() == "-10");
Так что беззнаковые типы и беззнаковая арифметика — не одно и то же. Совет "избегайте беззнаковой арифметики" не значит "избегайте беззнаковых типов".
Пётр Седов wrote: > C>Еще один пример где со знаковой арифметикой неудобно — это работа с > изображениями. > Изображения хранятся как массивы байтов (байт = unsigned char), чтобы > экономить память. Но беззнаковой арифметики опять нет, так как в > выражениях unsigned char продвигается (promote) до int (а не unsigned).
С чего бы? У меня, например, везде работа с ними ведется как с unsigned
char'ами, и никуда они не продвигаются.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Здравствуйте, remark, Вы писали:
R>>
R>> unsigned u = 0;
R>> int i = 0;
R>> u = i;
R>> i = u;
PD>// А вот это приведет к предупреждению
PD> if( i < u) ...
PD>// А вот это нет
PD> if( i == u) ...
R>>
PD>Это так, информация к размышлению...
Но, имхо, того что я привёл, уже достаточно, чтобы не использовать такое смешение типов.
Т.е. варнинг при сравнении, что мёртвому припарка
Здравствуйте, eao197, Вы писали: E>Здравствуйте, Аноним, Вы писали: А>>Да, во время написания кода было Signal = int. E>>>А тот, кто изменял тип Singal, скорее всего, даже не знал, какие арифметические операции над ним производятся. Обычная ситуация. E>Не то выделили. Нужно сделать упор на: какие арифметические операции над ним производятся. E>Вполне возможно, что автору изменений типа Signal был виден код, в котором над Signal использовалось только сложение.
Код с Signal содержал вычитание:
/**
* @return True if the input signals differ by more than the supplied
* tolerance, false otherwise.
*/bool
signalsDifferent(Signal s1, Signal s2, int tol)
{
return
s1 > s2 + tol || s1 < s2 - tol;
}
В статье не сказано, видел ли программист, заменивший int на unsigned, этот код.
Автор статьи (Thomas Guest) пишет, что одно из возможных решений — избавиться от вычитания:
We could recast the arithmetic so it works independently of signedness:
bool
signalsDifferent(Signal s1, Signal s2, int tol)
{
return
s1 > s2 + tol || s2 > s1 + tol;
}
kan предлагает вместо 'i < vec.size() — 1' писать 'i + 1 < vec.size()'. Но иногда после таких эквивалентных (с точки зрения обычной арифметики) преобразований код становится менее естественным. Я бы переписал функцию signalsDifferent так:
Код с вычитанием лучше, так как обе части неравенства имеют чёткий смысл. 'abs(Sig1 — Sig2)' — расстояние между сигналами. 'Tol' — максимальное расстояние между сигналами, при котором они ещё равны (примерно).
Парадокс: неотрицательность переменной — ещё не повод делать её unsigned.
Беззнаковая арифметика — это как ветрянка. Достаточно один раз переболеть (то есть потратить много времени и сил на поиск ошибки), и вырабатывается иммунитет на всю оставшуюся жизнь. Но есть редкие случаи, когда разумно использовать беззнаковую арифметику:
Вычисление hash-значения. Обычно это что-то вроде:
int CalcBucketIndex(const char pText[], int Len)
{
unsigned h = 0;
for (int i = 0; i < Len; i++)
{
h = 31 * h + pText[i];
}
return h % m_NumBuckets;
}
Здесь unsigned лучше int-а, так как не нужно обрабатывать отрицательные значения переменной h (возникают при переполнении int-а). Беззнаковая арифметика изолирована в функции.
Распределитель памяти. Размеры запрашиваемых блоков памяти, скорее всего, умещаются в int. Поэтому беззнаковую арифметику можно и нужно изолировать в классе распределителя.
Очень большие файлы. Скорее всего, базы данных.
16-битная платформа. 16-битного int-а может не хватить, а использовать 32-битный long не хочется, так как это затратно. Кстати, в статье может идти речь именно о такой платформе.
Желательно изолировать беззнаковую арифметику в функции или классе, это уменьшит риск ошибки.
Здравствуйте, Cyberax, Вы писали:
C>Пётр Седов wrote: >> C>Еще один пример где со знаковой арифметикой неудобно — это работа с изображениями. >> Изображения хранятся как массивы байтов (байт = unsigned char), чтобы экономить память. Но беззнаковой арифметики опять нет, так как в выражениях unsigned char продвигается (promote) до int (а не unsigned). C>С чего бы?
Это правило языка C++. Современные компиляторы следуют этому правилу.
C>У меня, например, везде работа с ними ведется как с unsigned char'ами, и никуда они не продвигаются.
Я уверен, что продвигаются. Это делается неявно. Например, есть такой код:
unsigned n = 10;
cout << n - 20 << endl;
cout << (n - 20 < 0) << endl;
Этот код выводит на консоль (MSVC6 Debug):
4294967286
0
Вместо -10 выводится "very big unsigned number" (в данном случае pow(2, 32) — 10). Такой "странный" результат получается из-за беззнаковой арифметики. Если заменить тип переменной n на unsigned char:
Пётр Седов wrote: > 4294967286 > 0 > Вместо -10 выводится "very big unsigned number" (в данном случае pow(2, > 32) — 10). Такой "странный" результат получается из-за беззнаковой > арифметики. Если заменить тип переменной n на unsigned char: > unsigned char n = 10; > cout << n — 20 << endl; > cout << (n — 20 < 0) << endl;
А теперь так:
unsigned char n = 10;
cout << n - 20 << endl;
n = (n - 20 < 0);
cout << n << endl;
Здравствуйте, Cyberax, Вы писали: C>А теперь так: C>
C>unsigned char n = 10;
C>cout << n - 20 << endl;
C>n = (n - 20 < 0);
C>cout << n << endl;
C>
Всё равно здесь нет беззнаковой арифметики. unsigned char продвигается до int и арифметика знаковая. И это хорошо, так как знаковая арифметика "ближе" к обычной, чем беззнаковая.