Механизм исключений является важным элементом большинства современных языков программирования. Предоставив единообразную схему для обработки всех видов сбоев в работе программ, исключения заменили собой многочисленные неформальные практики.
Тем не менее, при использовании исключений в конструкторах есть ряд тонких моментов. Так, не очевидно, как конструктор сможет перехватить исключение, выброшенное конструктором одного из полей этого класса. Здесь мы обсудим эту проблему и её решение в контексте языка программирования С++.
Постановка проблемы
Рассмотрим простую программу, в которой определяется класс String
— последовательность символов заданной длины. Экземпляр String
инициализируется в конструкторе набором пробелов. Клиенты класса String
могут получать или изменять содержимое определённого символа в строке, указав его индекс. Для этого в String
задан operator[]
.
#include <cstdlib>
#include <iostream>
using std::cout; using std::endl;
using std::cerr;
struct BufError { };
struct Buf
{
char* array_;
Buf(int sz)
{
array_ = (char*) malloc(sz);
if(array_ == 0)
throw BufError();
}
~Buf()
{
free(array_);
}
char& at(int offset)
{
return array_[offset];
}
};
struct StringError { };
struct String
{
int limit_;
Buf buf_;
String(int limit) : limit_(limit), buf_(limit)
{
for(int i = 0; i < limit; ++i)
(*this)[i] = ' ';
}
char& operator[](int offset)
{
if(offset < 0 || offset >= limit_)
throw StringError();
else
return buf_.at(offset);
}
};
int main(int argc, char* argv[])
{
try
{
String s(200);
s[0] = 'A';
cout << s[0] << endl; // Output: 'A'
}
catch(BufError& be)
{
cerr << "Some String failure" << endl;
}
catch(StringError& se)
{
cerr << "Some String failure" << endl;
}
}
Проанализировав код String
, мы увидим что он использует поле buf_
класса Buf
для непосредственной работы с памятью, в которой хранятся символы.
Компилятор предполагает, что все непримитивные типы данных инициализируются с помощью своих конструкторов. Поскольку конструктора по умолчанию у Buf
нет, то его конструктор явным образом вызывается в конструкторе String
.
Чтобы уяснить суть проблемы, предположим что память, доступная для выделения, закончилась (можно закомментировать if
в строке 16). При этом конструктор Buf
выбросит исключение типа BufError
, которое будет распространяться благодаря раскрутке стека, до тех пор, пока не будет перехвачено в блоке catch(BufError& be)
функции main()
. Однако, если посмотреть на main()
внимательнее, то станет понятно, что нет никакой разницы между исключениями BufError
и StringError
: оба они сигнализируют о какой-то проблеме в локальной переменной s
.
Очевидно, что код main()
выглядел бы проще и понятнее, если бы обрабатывал только один тип исключения, а именно StringError
, вместо двух различных типов. Учитывая, что не всегда возможно изменять исходный код библиотеки классов (в частности, изменить тип выбрасываемых исключений), общее решение требует, чтобы класс String
мог преобразовать исключения BufError
в исключения StringError
.
Решение
Возникает вопрос, как поместить список инициализации полей внутрь блока try
. Такое средство в стандарте языка С++ существует и называется function-try-block.
Следующий код использует function-try-block для преобразования исключения BufError
в исключение StringError
. Обратите внимание на ключевое слово try
в строке 39, добавленное в конструкторе String
.
#include <cstdlib>
#include <iostream>
using std::cout; using std::endl;
using std::cerr;
struct BufError { };
struct Buf
{
char* array_;
Buf(int sz)
{
array_ = (char*) malloc(sz);
if(array_ == 0)
throw BufError();
}
~Buf()
{
free(array_);
}
char& operator[](int offset)
{
return array_[offset];
}
};
struct StringError { };
struct String
{
int limit_;
Buf buf_;
String(int limit)
try // function-try-block begins here
:
limit_(limit), buf_(limit)
{
for(int i = 0; i < limit; ++i)
(*this)[i] = ' ';
} // function-try-block ends here
catch(BufError& )
{
throw StringError();
}
char& operator[](int offset)
{
if(offset < 0 || offset >= limit_)
throw StringError();
else
return buf_[offset];
}
};
int main(int argc, char* argv[])
{
try
{
String s(200);
s[0] = 'A';
cout << s[0] << endl; // Output: 'A'
}
catch(StringError& se)
{
cerr << "Some String failure" << endl;
}
}
Замечания
Совместимость. Не все компиляторы поддерживают function-try-block. GCC делает это уже давно, а Microsoft Visual C++ — лишь в последних версиях.
Применение function-try-block не ограничивается конструкторами. Обычная функция или функция-член класса также могут их использовать. Например, функция divide()
, представленная ниже, возвращает 0, если в ходе её выполнения генерируется исключение, гарантируя, что вызовы вроде divide(5,0)
дадут 0 в результате. Тем не менее, для функций-членов особой нужды в использовании function-try-block нет, так как последние равносильны «обычным» блокам try-catch
, где try
начинается непосредственно перед первой инструкцией функции и заканчивается сразу после последней её инструкции.
static int divide(int x, int y)
try
{
return x / y;
}
catch(...)
{
return 0;
}
Исключения инициализации не могут быть скрыты. function-try-block в конструкторе обязан сгенерировать исключение сам или повторно выбросить то, что было им перехвачено. Он не может просто «проглотить» исключение, а затем продолжить выполнение программы, как ни в чём ни бывало. Даже если вы ничего не укажите в части catch
, принадлежащей function-try-block, компилятор сгенерирует перехваченное исключение повторно, сразу после завершения выполнения catch
. Следовательно, две версии класса А, показанные ниже, эквивалентны.
// Version 1
struct A
{
Buf b_;
A(int n)
try
:
b_(n)
{
cout << "A initialized" << endl;
}
catch(BufError& )
{
cout << "BufError caught" << endl;
}
};
// Version 2
struct A
{
Buf b_;
A(int n)
try
:
b_(n)
{
cout << "A initialized" << endl;
}
catch(BufError& be)
{
cout << "BufError caught" << endl;
throw;
}
};
Статья представляет собой пересказ работы, с исправлением мелких ошибок в коде. Полезная информация по теме содержится также в книге Лаптев В. В. C++. Объектно-ориентированное программирование: Учебное пособие. — СПб.: Питер, 2008. — 464 с. (Глава 7, Исключения в списке инициализации конструктора).
Комментарии
comments powered by Disqus