В стародавние времена ошибки можно было обработать только одним способом. Мы, кстати, использовали именно его. Речь идёт о кодах ошибок. Это когда функция возвращает специальное значение, проверив которое, можно узнать - выполнилась функция как надо или нет. Рассмотрим пример:

код на языке c++
typedef int ERR_CODE;
const ERR_CODE OK = 0;
const ERR_CODE FAIL = 1;

ERR_CODE example(int x)
{
  if (x > 0)
  {
    return OK;
  }
  else
  {
    return FAIL;
  }
}

int main()
{
  ERR_CODE error;
  int var;
  cin >> var;
  error = example(var);
  if (error == FAIL)
    return 0;

  // остальная часть программы

  return 0;
}

В данном примере мы создали простейшую систему кодов ошибок: OK - всё в порядке, FAIL - произошла какая-то ошибка. Заметьте, что обе константы имеют тип int. Но для наглядности мы переопределили тип int и "создали" новый тип ERR_CODE.

Вот как раз с подобной системой и работает WinAPI. Вместо ERR_CODE там HRESULT. Вместо OK и FAIL - S_OK и E_FAIL. Конечно же там есть и другие коды ошибок.

HRESULT - 32-ухбитное число (int). Код S_OK представлен как ноль. В HRESULT закодировано несколько значений. В первых (справа налево) двух байтах расположен код ошибки. Коды системных ошибок можно посмотреть здесь (на английском). Загляните, загляните!!!

Далее идут одиннадцать байт занимаемых под объект. Т.е. какой программный объект стал виной ошибки.

Затем идут четыре зарезервированных на будущее бита.

И последний бит определяет успех или неудачу. То есть именно этот бит самый главный.

Например S_OK выглядит вот так: 0x00000000. Здесь число представлено в шестнадцатиричном формате (мне лень писать 32 нуля :) ). О том, что число в шестнадцатиричном формате говорит 0x перед этим числом. Каждая цифра представляет собой 4 цифры в двоичном формате. Соответственно две цифры представляют собой 8 бит - один байт.

E_FAIL выгляди вот так: 0x80004005 - 10000000 00000000 00000100 00000101. Самый старший бит говорит о неудаче, так как установлен в единицу.

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

Проблемы с кодами ошибок

Первая проблема: Так как функция возвращает код, то мы не можем вернуть "нужное" значение. Эта проблема решается передачей в функцию значения по ссылке или по указателю. Все методы, которыми мы будем пользоваться в Direct3D получают аргументы по указателям.

Разберём маленький пример: у нас есть указателЬ на объект IDirect3D9* d3d и заполненная структура D3DPRESENT_PARAMETERS pp. Чтобы получить указатель на устройство нужно воспользоваться следующим кодом:

d3d->CreateDevice(NULL, тип устройства, окно, флаги,
&pp, &dev);

dev - указатель на устройство.

Так как метод IDirect3D9::CreateDevice возвращает HRESULT, то чтобы создать устройство нужно передавать в функцию указатель на dev. Если бы DirectX не использовал HRESULT, то вышеприведённый код мог бы выглядеть, например, вот так:

dev = d3d->CreateDevice(NULL, тип устройства, окно, флаги, &pp);
Вторая проблема: Код становится слишком перегруженным ненужными деталями, так как после каждого вызова функции нужно проверять, какое она вернула значение. Вернёмся к предыдущему примеру, вот как можно обработать ошибку:

код на языке c++
if (d3d->CreateDevice(NULL, тип устройства, окно, флаги,
                  &pp, &dev) == S_OK)
{
  // метод выполнился успешно
}

Правда, используется не совсем такой код. Но это пока не важно.

Отладочный макрос _ASSERT

В отладочном режиме можно использовать дополнительное средство по обработке ошибок - отладочный макрос _ASSERT (assertion - утверждение). Макрос _ASSERT доступен только в отладочном режиме, в релизном - компилятор не обращает на этот макрос внимания. Работает этот макрос следующим образом:

_ASSERT( var > 0 );
Если условие выполняется, то программа продолжает работать. Если же условная операция возрващает false, то программа выдаст сообщение об ошибке, и в диалоговом окне вы сможете выбрать - что делать дальше: завершить программу, попытаться исправить ошибку или пропустить ошибку.

Использовать _ASSERT очень удобно с простыми условиями. Для использования этого макроса нужно добавить заголовочный файл crtdbg.h.

Ещё для обработки ошибок использовались функции setjmp и longjmp, но я их уже давно не встречал в коде.

Обработка исключений (exceptions handling)

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

Блок повторных попыток (try), обработчик исключений (catch) и генерация исключений (throw)

Исключения прежде всего необходимо использовать с классами. При работе с исключениями необходимо выучить три новых ключевых слова: try (попытка, пробовать), throw (бросать) и catch (ловить).

Рассмотрим небольшой пример, где, если пользователь введёт значение меньше нуля, то будет сгенерирована ошибка:

код на языке c++
class warrior
{
private:
  int health;

public:
  class error
  { };

  void set_health()
  {
    cin >> health;
    if (health < 0)
      throw error();
  }
};

int main()
{
  setlocale(LC_CTYPE,"Russian");
  try
  {
    warrior a;
    a.set_health();
  }
  catch (warrior::error)
  {
    cout << "Пользователь ввёл неверное значение!\n";
  }
  _getch();
  return 0;
}

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

В коде встречается два блока: try и catch. В блоке try идёт набдлюдение за появлением исключений. Как только было сгенерировано исключение, управление передаётся в блок catch, в котором обрабатывается ошибка. Исключения генерируются с помощью слова throw после которого, в скобках вызывается конструктор специального класса исключений. Этот специальный класс исключений - error (имя может быть любым). Мы определили его классе warrior. Если бы мы не создавали error, то тогда нужно было бы использовать класс исключений по умолчанию. Он называется exception. Вот как выглядел бы пример без класса error (привожу только фрагменты, всё остальное - без изменений):

код на языке c++
class warrior
{
private:
  int health;

public:
  void set_health()
  {
    cin >> health;
    if (health < 0)
      throw exception();
  }
};
//-------------------------
catch (exception)
{
  cout << "Пользователь ввёл неверное значение!\n";
}
Пользовательские классы исключений необходимы когда  в блок catch нужно передать аргументы:

public:
  class error
  {
  public:
    int data;
    error(int d) : data(d) {}
  };
  void set_health()
  {
    cin >> health;
    if (health < 0)
      throw error(health);
//-------------------------
catch (warrior::error e)
{
  cout << "Пользователь ввёл неверное значение: " << e.data << "\n";
}

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

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