Date Редакция Категория comp Теги Cpp

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

Тем не менее, при использовании исключений в конструкторах есть ряд тонких моментов. Так, не очевидно, как конструктор сможет перехватить исключение, выброшенное конструктором одного из полей этого класса. Здесь мы обсудим эту проблему и её решение в контексте языка программирования С++.

Постановка проблемы

Рассмотрим простую программу, в которой определяется класс 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