Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 4.

Статическая отладка программ

Запись от Evg размещена 15.02.2012 в 23:26
Обновил(-а) Evg 05.02.2013 в 20:48

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

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

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

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

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




  • 1. Введение
  • 2. Внутренний контроль допустимых значений (ASSERT'ы)
    2.1. Пример поздно проявляющейся ошибки
    2.2. Включаем контроль правильности входных данных
    2.3. Общее понятие ASSERT'а
    2.4. Реализация ASSERT'а для начинающих
    2.5. Реализация ASSERT'а для продвинутых
    2.5.1. Заключаем тело в фигурные скобки
    2.5.2. Удаляем экранирующие круглые скобки
    2.5.3. Пытаемся устранить предупреждения компилятора
    2.5.4. Итоговая продвинутая реализация ASSERT'а
    2.6. Где нужно ставить ASSERT'ы
    2.7. Тонкости работы с ASSERT'ами
  • 3. Внутренний контроль недостижимых участков кода (FATAL'ы)
  • 4. Внутренний контроль целостности данных (магические числа)
  • 5. Внутренние и внешние ошибки
  • 6. Что делать при срабатывании внутреннего контроля
  • 7. Трассировка работы программы
  • 8. Ссылки на темы, где обсуждался данный вопрос






1. Введение

FIXME написать






2. Внутренний контроль допустимых значений (ASSERT'ы)

2.1. Пример поздно проявляющейся ошибки

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

Код:
sum = a[0]*b[0] + a[1]*b[1] + ... + a[N]*b[N];
Массивы a и b могут представлять собой некоторые физические величины или коэффициэнты. Допустим, что инициализация данных массивов размазана по разным местам программы. Например, коэффициэнты a задаются вручную, коэффициэнты b считываются из файла и т.п. Большой пример я писать не буду, а сделаю маленькую программку, в которой инициализацию массивов выделю в разные функции. Пусть в нашем случае N=4, a={1,1,1,1}, b={10,20,30,40}. Нетрудно посчитать, что итоговая сумма должна равняться 100.

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
 
#define N 4
 
int a[N], b[N];
 
void
init_a (int i)
{
  a[i] = 1;
}
 
void
init_b (int i)
{
  b[i] = 10 * (i + 1);
}
 
int
main (void)
{
  int i, sum;
 
  for (i = 0; i <= N; i++)
    init_b (i);
 
  for (i = 0; i <= N; i++)
    init_a (i);
 
  sum = 0;
  for (i = 0; i < N; i++)
    sum += (a[i] * b[i]);
 
  printf ("sum = %d\n", sum);
  return 0;
}
Если исполнить эту программу, то, например, при использовании gcc напечатается итоговое значение 91 вместо 100. Стало быть, программа содержит ошибку, но ошибка эта не сломала процесс исполнения программы, а выразилась в неправильном результате. Кто-то сразу заметил, что в ошибка заключается в неправильном написании циклов. Кто-то этот факт быстро найдёт при помощи отладчика. Но так или иначе мы имеем одну из весьма неприятных ошибок - порчу данных в результате ошибочного написания кода программы. При выходе за границу массива a мы имели такую ситуацию, что при записи в элемент a[4] реально произошла запись в b[0], поскольку эти массивы компилятор расположил в памяти один за другим

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

2.2. Включаем контроль правильности входных данных

Из-за чего произошла ошибка в примере из прошлого раздела? Из-за того, что в функциях init_a и init_b мы провели некоторые вычисления, полагая при этом, что на вход функции будет подано правильное значение i. Для раннего обнаружения ошибки можно было бы в эти функции включить контроль за правильностью параметра i. Например, функция init_a могла бы выглядеть так:

C
1
2
3
4
5
6
7
8
void
init_a (int i)
{
  if (i < 0 || i >= N)
    abort();
 
  a[i] = 1;
}
Функцию b следовало бы модифицировать аналогичным образом. При таком раскладе вылет в abort во время исполнения призошёл бы намного ближе к точке реального возникновения ошибки. Во всяком случае, встав в отладчике в точку падения, было бы очень просто понять причину ошибки и найти точку возникновения ошибки. В случае долго работающей программы мы очевидным образом сэкономили бы кучу времени и нервов, затратив немного усилий для написания всего пары строк дополнительного кода.

2.3. Общее понятие ASSERT'а

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

Начнём с самого простого. Мы видим, что все наши if'ы выглядят схожим образом: if (условие) abort. Во избежание копипэйстов общие части можно засунуть в тело макроса.

C
1
2
3
4
5
6
7
8
9
10
#define ASSERT(expr) \
  if (! (expr)) \
    abort();
 
void
init_a (int i)
{
  ASSERT (i >= 0 && i < N);
  a[i] = 1;
}
Обратите внимание на то, что условие теперь у нас обратное к условию, которое мы строили через if'ы. Почему так? Assertion означает "предположение". А запись "ASSERT (i >= 0 && i < N)" следует читать, как "предполагаем, что i >= 0 && i < N". Т.е. макрос выполнен таким образом, что если условие истинное, то макрос ничего не делает, а если ложное - то ломает программу на исполнении. По опыту знаю, что многим начинающим такая инверсность условия не по душе и они реализовывают макросы с обратными условиями. Это, конечно, дело вкуса, но такие люди постоянно наступают на грабли из-за того, что плывут против течения (т.е. делают не так, как принято делать программистами во всём мире).

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

C
1
2
3
4
5
6
7
#if 1
  #define ASSERT(expr) \
    if (! (expr)) \
      abort();
#else
  #define ASSERT(expr)
#endif
Заменив единичку на нолик в первой строке мы сразу же отключим весь контроль. Нужно это делать, или нет - в каждом конкретном случае должен решать программист. Если мы имеем программу, от которой требуется предельная производительность в режиме реального времени, то весь контроль в "боевом" режиме лучше отключать. Если наличие контроля ничему не противоречит, то лучше его оставить. Например, в исходниках компилятора gcc основная часть таких конструкций включена в том числе и в боевой версии компилятора. Из тех соображений, что в "тяжёлых" случаях пусть лучше компилятор выломается с диагностикой "внутренняя ошибка компилятора", чем сгенерирует неверный код.

Естественно, что в каждой конкретной программе будет свой критерий выбора, какую из реализаций ASSERT'а цеплять. Управление выбором через нолик или единичку я привёл лишь условно. На будущее при написании реализации ASSERT'а я буду опускать реализацию-пустышку, чтобы не разводить ненужный мусор

2.4. Реализация ASSERT'а для начинающих

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

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
 
#define N 4
 
int a[N], b[N];
 
#define ASSERT(expr) \
  if (! (expr)) \
    abort();
 
void
init_a (int i)
{
  ASSERT (i >= 0 && i < N);
  a[i] = 1;
}
 
void
init_b (int i)
{
  ASSERT (i >= 0 && i < N);
  b[i] = 10 * (i + 1);
}
 
int
main (void)
{
  int i, sum;
 
  for (i = 0; i <= N; i++)
    init_b (i);
 
  for (i = 0; i <= N; i++)
    init_a (i);
 
  sum = 0;
  for (i = 0; i < N; i++)
    sum += (a[i] * b[i]);
 
  printf ("sum = %d\n", sum);
  return 0;
}
По текущему состоянию в случае срабатывания ASSERT'а мы видим лишь унылую надпись о том, что программа aborted.

Препроцессор имеет встроенные макросы __FILE__ и __LINE__, которые заменяются на имя файла и номер строки в месте их возникновения. Используя эти конструкции макрос ASSERT можно переписать следующим образом

C
1
2
3
4
5
6
#define ASSERT(expr) \
  if (! (expr)) \
    { \
      fprintf (stderr, "assertion failed at %s:%d\n", __FILE__, __LINE__); \
      exit (1); \
    }
Теперь наша программа начнёт падать с несколько более удобным текстом: "assertion failed at t.c:26". В тексте засветилось имя файла и номер строки, соответствующие точке, где установлен сработавший ASSERT. Таким образом мы уже без отладчика знаем, в каком месте сработал контроль. И во многих случаях сможем быстро исправить ошибку, не прибегая к помощи отладчика, а заглянув лишь в указанное место исходника и поняв суть проблемы.

Едем дальше. При реализации тела макроса при помощи оператора # можно превратить параметр макроса в строковой вид. Напишем наш макрос следующим образом:

C
1
2
3
4
5
6
#define ASSERT(expr) \
  if (! (expr)) \
    { \
      fprintf (stderr, "assertion \"%s\" failed at %s:%d\n", #expr, __FILE__, __LINE__); \
      exit (1); \
    }
Теперь текст ошибки у нас становится совсем удобным: "assertion "i >= 0 && i < N" failed at t.c:26". В тексте засветилась не только привязка к исходнику, но и текст контролируемого выражения. При таком раскладе в целом ряде случаев нам не придётся даже в исходник программы заглядывать, потому что текста выражения нам может оказаться вполне достаточно, чтобы понять причину ошибки

2.5. Реализация ASSERT'а для продвинутых

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

2.5.1. Заключаем тело в фигурные скобки

При частом использовании ASSERT'а рано или поздно вы можете столкнуться с ситуацией наподобие следующей:

C
1
2
3
4
5
6
7
if (x == 0)
  <действие>
else if (x == 1)
  /* Здесь ничего не делаем, но просто сделаем контроль за значением y */
  ASSERT (y > 0);
else
  <действие>
Макрос ASSERT внутри себя содержит оператор if. Поэтому оператор else из 6-ой строки данного примера на самом деле отнесётся к if'у, который находится внутри ASSERT'а, а не к "else if" из 3-ей строки

Самое тупое решение - это заключить в фигурные скобки действие 5-й строки (т.е. точку подстановки ASSERT'а). Решение тупое, потому что такое решение может идти вразрез с стилевыми соглашениями, не говоря уж о том, что об этом можно банально забыть. Следующее, что приходит на ум - это затащить фигурные скобки в тело макроса ASSERT:

C
1
2
3
4
5
6
7
8
#define ASSERT(expr) \
  { \
    if (! (expr)) \
      { \
        fprintf (stderr, "assertion \"%s\" failed at %s:%d\n", #expr, __FILE__, __LINE__); \
        exit (1); \
      } \
  }
Но это не выход. Потому как при раскрытии макроса у нас для строк 3-6 получится следующее:

C
1
2
3
else if (x == 1)
  { <внутренности assert> };
else
фигурные скобки являются составным оператором, а потому отнесутся к if'у. Далее у нас идёт точка с запятой, что по синтаксису будет означать, что оператор if закончился и начался новый пустой оператор. А затем идёт else, что приведёт к синтаксической ошибке, потому как оператор if номинально уже закончился. Следующей очевидной попыткой будет не ставить точку с запятой после ASSERT'а. И действительно, в данном случае это нас спасёт. Однако если мы превратим тело макроса ASSERT в пустышку, то мы получим конструкцию

C
1
2
else if (x == 1)
else
что так же является синтаксической ошибкой.

Выходом из такой ситуации служит хак (или чит) - заключить тело макроса в цикл do-while, у которого исполнится только одна итерация

C
1
2
3
4
5
6
7
8
#define ASSERT(expr) \
  do { \
    if (! (expr)) \
      { \
        fprintf (stderr, "assertion \"%s\" failed at %s:%d\n", #expr, __FILE__, __LINE__); \
        exit (1); \
      } \
  } while (0)
Обратите внимание, что на конце нет точки с запятой (которую инстинктивно хочется поставить). Такая реализация макроса не содержит тех недостатков, которые описаны в данном разделе. А любой приличный оптимизирующий компилятор цикл удалит из-за условия while(0)

2.5.2. Удаляем экранирующие круглые скобки

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

C
1
2
3
r = func (x, y);
/* В нашем случае ошибочных результатов быть не должно */
ASSERT (r = 0);
Ошибка классическая: вместо сравнения "==" автор написал присваивание "=". Большинство современных компиляторов умеют выдавать предупреждение на конструкции типа "if (x = 0)", когда есть подозрение, что автор имел в виду сравнение, а не присваивание. Мы использовали компилятор gcc, который замечательно ловит такую ситуацию, а потому никакой засады не ожидали.

Однако засада есть и кроется она вот в чём. Хоть и редко, но бывают случаи, когда на месте операций сравнения действительно находится присваивание и автор хочет сделать присваивание и сравнение результата с нулём. А потому компилятор должен предоставить программисту возможность написать такой код таким образом, чтобы не возникало предупреждений. По негласным соглашениям для таких случаев программист должен экранировать присваивание дополнительными круглыми скобками. Т.е. если написать "if (x = 0)", то компилятор выдаст предупреждение, а если написать "if ((x = 0))" - то нет.

В нашем случае при реализации макроса так же использовалось дополнительное экранирование круглыми скобками. Без него нельзя гарантировать, что оператор "!" будет вычислен самым последним. С виду единственным надёжным выходом из ситуации является написание if'а без восклицательного знака, чтобы не приходилось делать экранирование круглыми скобками. В данном случае я опущу конструкцию do-while из предыдущего раздела.

C
1
2
3
4
5
6
7
8
#define ASSERT(expr) \
  if (expr) \
    /* nothing */ ; \
  else \
    { \
      fprintf (stderr, "assertion \"%s\" failed at %s:%d\n", #expr, __FILE__, __LINE__); \
      exit (1); \
    }
2.5.3. Пытаемся устранить предупреждения компилятора

Недавно на работе мы начали переход со сборки проекта компилятором gcc-3.4.6 на более современную версию компилятора gcc-4.x.x. В ситуации из предыдущего раздела

C
1
2
r = func (x, y);
ASSERT (r == 0);
мы столкнулись с следующим неприятным моментом. В боевом режиме, когда макрос ASSERT превращается в пустышку, у нас получается ситуация, кода в переменную r было записано значение, но переменная больше не использовалась (т.е. единственное использование в макросе ASSERT было отключено). На такую ситуацию современные версии gcc выдают предупреждение. Предупреждение можно отключить, но тогда оно отключится в том числе и в "обычных" случаях (не в ASSERT'ах), чего не хотелось бы. Порывшись в интернете, мы нашли одно из решений, но, забегая вперёд, сразу же скажу, что оно не совсем честное. В данном случае нужно будет изменить вторую реализацию ASSERT'а (ту, где ранее мы делали пустышку):

C
1
2
#define ASSERT(expr) \
  (void)(0 && (expr))
При такой реализации всё выражение компилятор выкинет как мёртвый код, т.к. он стоит вторым операндом в "0 && ...", в то же время номинально в тексте выражение останется, а потому указанных выше предупреждений не будет. Нечестность данного метода заключается в том, что реализация ASSERT'а должна быть такой, что в режиме с отключенным контролем макрос обязан обращаться в пустышку. В простых случаях это роли не играет, но вот в случаях типа "ASSERT (check (x, y) == 0)" может стрельнуть тем, что в боевой версии интерфейс check выключен, а потому такая реализация ASSERT'а не пройдёт через компиляцию.

В рамках одного конкретного проекта на данное ограничение можно плюнуть с высокой колокольни. Но вот если делается какая-то пользовательская библиотека, то такое решение, к сожалению, не прокатит. В частности, когда мы искали ответ на вопрос, то мы натолкнулись на обсуждение реализации данного макроса в заголовочном файле assert.h, входящего в состав стандартной поставки gcc. В Си++ так же есть стандартный заголовочной файл cassert, внутренности которого должны соответствовать данному требованию.

2.5.4. Итоговая продвинутая реализация ASSERT'а

Теперь можно написать итоговую продвинутую реализацию макроса ASSERT:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if 1
 
  /* Отладочная версия */
  #define ASSERT(expr) \
    do { \
      if (expr) \
        /* nothing */ ; \
      else \
        { \
          fprintf (stderr, "assertion \"%s\" failed at %s:%d\n", #expr, __FILE__, __LINE__); \
          exit (1); \
        } \
    } while (0)
 
#else
 
  /* Боевая версия */
  #define ASSERT(expr) \
    (void) (0 && (expr))
 
#endif
2.6. Где нужно ставить ASSERT'ы

Если у человека возник интерес к использованию ASSERT'ов, то сразу же появляется вопрос: а где их лучше ставить. Универсального ответа на этот вопрос нет. Всё должно исходить из личного опыта. Так или иначе программист всегда закладывается на какие-то предпосылки: предполагаем, что переменная положительная, предполагаем, что переменная является индексом массива и находится в диапазоне, предполагаем, что здесь мы работаем только с значениями enum'а и т.п. По началу можно работать таким образом, что ставить ASSERT'ы только в тех местах, где возникла реальная ошибка. Со временем появится некоторое внутреннее ощущение того, в каких местах лучше всего расставлять ASSERT'ы.

По тем или иным соображениям, бывают ситуации, когда приходится вводить некоторые понятия типа "приоритеты ASSERT'а". С ходу на пальцах понятную ситуацию придумать не получается, но такие случаи действительно бывают. В этом случае можно реализовать систему ASSERT'ов по приоритетам, как это написано здесь или здесь

FIXME Про приоритеты добавить примеры, потому что неудобно читать, когда надо скакать по ссылкам

2.7. Тонкости работы с ASSERT'ами

FIXME Написать по поводу того, что параметром ASSERT'а нельзя делать действие
FIXME Написать про альтернативные тестовые макросы http://www.cyberforum.ru/faq/thread330221.html#post1906409






3. Внутренний контроль недостижимых участков кода (FATAL'ы)

В программировании довольно часто встречаются случаи, когда в программе имеются участки кода, в которые мы не должны попасть. Классический пример на эту тему - switch, ключом которого является значение типа enum:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum MyEnum
{
  MY_ENUM_A,
  MY_ENUM_B,
  MY_ENUM_C
};
...
int
myfunc (enum MyEnum e)
{
  int x;
 
  switch (e)
  {
    case MY_ENUM_A: x = 10; break;
    case MY_ENUM_B: x = 20; break;
    case MY_ENUM_C: x = 30; break;
  }
 
  return myfunc2 (x);
}
В данном коде содержится ветка исполнения, которая в тексте программы не присутствует, но тем не менее неявно в коде она есть и компилятор для неё построит код - это ветка default. В нашем примере если в функцию придёт параметр, значение которого не попадает в enum (такое может случиться в результате ошибки программиста), то исполнение программы пойдёт по неявной ветке default и переменная x окажется не инициализированной, что приведёт к неизвестным последствиям при работе программы. Звёзды могут встать так, что значение переменной x постоянно оказывается "примерно хорошим" и внешне эта ошибка не проявляется. Но в один прекрасный момент звёзды могут встать по другому и ошибка даст о себе знать. Причём может так случиться, что опять проявления ошибки может находиться далеко от точки возникновения ошибки.

Чтобы не наступать на грабли в такой ситуации, нужно явным образом включить ветку исполнения в текст программы и поставить в ней ловушку:

C
1
2
3
4
5
6
7
8
9
switch (e)
{
  case MY_ENUM_A: x = 10; break;
  case MY_ENUM_B: x = 20; break;
  case MY_ENUM_C: x = 30; break;
  default:
    printf ("Сюда попасть не должны\n");
    abort();
}
При таком раскладе у нас в тексте программы в явном виде присутствуют все ветви исполнения. Ветвь исполнения, в которую по замыслу программиста никогда не должны попасть, явным образом обозначена.

Конечно же в каждое такое место вставлять вызов printf'а (или что нам там нужно) неудобно, а потому по аналогии с макросом ASSERT нужно реализовать какой-нибудь макрос. Обычно его называют FATAL. Самая простая реализация данного макроса выглядит как:

C
1
2
3
4
5
#define FATAL() \
  ASSERT (0)
...
  default: FATAL(); break;
...
однако такая реализация в некоторых случаях может вызвать неудобства. Макрос ASSERT внутри себя содержит оператор if, в котором в данном случае будет находиться константное условие. Некоторые особо умные компиляторы начнут выдавать в этом месте предупреждение. Поэтому проще всего сделать небольшой копипэйст из внутренностей макроса ASSERT и симметричным образом реализовать макрос FATAL:

C
1
2
3
4
5
6
7
8
9
10
11
12
#if 1
 
  /* Отладочная версия */
  #define FATAL() \
    fprintf (stderr, "internal programm error at %s:%d\n", __FILE__, __LINE__)
 
#else
 
  /* Боевая версия */
  #define FATAL()
 
#endif
Макрос FATAL не содержит ни параметров, ни условных операторов, а потому всяких тонкостей, присущих ASSERT'у (описанных в разделе 2.5) здесь нету.

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

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int arr[10];
 
int
get_index (int val)
{
  int i;
 
  for (i = 0; i < 10; i++)
    if (arr[i] == val)
      return i;
 
  /* Сюда попасть не должны */
  FATAL();
  return -1000000;
}
Обратите внимание на то, что в недостижимой ветке я всё равно реализовал код (return -1000000). Компилятор не знает о том, что в ту точку по логике исполнения мы попасть не должны, а потому какой-то код он построить должен. Если там ничего не писать, то компилятор либо начнёт ругаться (на предмет того, что в функции отсутствует return), либо просто построит возврат неинициализированного значения. При написании кода в недостижимой ветке я руководствовался следующими соображениями. В боевой версии программы FATAL у нас может быть пустышкой. Поэтому реализуем такой код, который заведомо (или с большой вероятностью) сломает программу на исполнении как можно ближе к точке возникновения ошибки. Функция возвращает индекс массива, а потому я вернул "плохое" значение, которое будучи использованное как индекс массива сломает программу при обращении в память (ибо при таком значении индекса мы почти наверняка залезем в недопустимую память).

Точно так же в ветке default в программе со switch'ом (написанной в начале данного раздела) в переменную x можно было бы записать "плохое" значение, которое в случае пустой реализации FATAL'а так же поломало бы исполнение программы как можно ближе к точке возникновения ошибки.






4. Внутренний контроль целостности данных (магические числа)

FIXME Написать






5. Внутренние и внешние ошибки

FIXME Написать






6. Что делать при срабатывании внутреннего контроля

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

Я уже писал о том, что ASSERT может работать в отладочном режиме (контроль включен) или в боевом режиме (контроль выключен). Так же я писал и о том, что ASSERT желательно оставлять в боевом режиме, если к этому нет каких-то противопоказаний (скорость работы, требования заказчика и т.п.). Это должно облегчить сопровождение программы, у которой много пользователей и имеется большая комбинация различных входных данных (т.е. когда программист собственным тестированием не в состоянии покрыть все жизненные ситуации). Поэтому включенный внутренний контроль в момент работы программы у пользователя - это ещё одна статья тестирования. И в момент срабатывания ASSERT'а программа должна вывести некоторую полезную (для программиста) информацию и просьбу к пользователю передать эту информацию разработчику с описанием ситуации, в которой воспроизвелась ошибка.

В нашем примере внутри ASSERT'а мы выводили печать в консольном режиме и завершали работу программы. Но в каждой конкретной программе действия могут выполняться разные.

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

У нас на работе в ASSERT'ы встроена печать стека. Можно так же делать распечатку наиболее важных глобальных переменных, по значениям которых программист может либо локализовать ошибку, либо сразу же выяснить причину ошибки. Было бы полезно реализовать механизм, при помощи которого пользователю было бы просто и удобно отправить разработчику всю ту информацию, которую выдала программа в момент срабатывания ASSERT'а. Например, идеальным вариантом для GUI-программы была бы кнопочка "отправить отчёт", при нажатии на которую выскакивало бы окно почтового клиента, в котором уже был бы сформирован полный текст письма, а пользователю осталось бы только нажать на "Отправить".

В общем случае действия по срабатыванию ASSERT'а определяются разработчиком программы, его фантазией и желанием сделать программу более удобной для пользователя даже в тех случаях, когда происходит сбой работы программы. Именно поэтому я НЕ использую стандартные реализации из стандартных заголовочных файлов assert.h или cassert, а всегда делаю свою реализацию






7. Трассировка работы программы

FIXME Написать
FIXME "if (DEBUG)" vs "#ifdef DEBUG"






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

assert.h
Просмотров 7622 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru