(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Что нового в работе с исключениями в C++11

Источник: habrahabr
prograholic

В интернете довольно много говорят о новых возможностях C++11: auto, lambda, variadic templates. Но как-то обошли стороной новые возможности работы с исключениями, которые предоставляет язык и стандартная библиотека.

От предыдущей версии стандарта остался механизм генерации исключений (throw), проверка того, что мы находимся в процессе обработки исключения (std::uncaught_exception), механизм остановки, если исключение не было обработано. Также есть иерархия стандартных исключений на базе класса std::exception.

Новый стандарт добавляет к этим вещам еще несколько сущностей, которые, на мой взгляд, могут существенно упростить работу с исключениями в C++.

exception_ptr

Итак, самое первое, с чем мы можем столкнуться - это std::exception_ptr. Этот тип позволяет в себе хранить исключение абсолютно любого типа. Стандарт не оговаривает каким образом получен этот тип. Это может быть typedef, это может быть реализация класса. Его поведение сходно поведению std::shared_ptr, то есть его можно копировать, передавать в качестве параметра, при этом само исключение не копируется. Основное предназначение exception_ptr - это передача исключений в качестве параметров функции, возможна передача исключений между потоками. Таким образом, объекты данного типа позволяют сделать обработку ошибок более гибкой:
struct some_exception {
    explicit some_exception(int x): v(x) {
        std::cout << " int ctor" << std::endl;
    }

    some_exception(const some_exception & e): v(e.v) {
        std::cout << " copy ctor" << std::endl;
    }

    int v;
};

std::exception_ptr throwExceptionAndCaptureExceptionPtr() {
    std::exception_ptr currentException;
    try {
        const int throwValue = 10;
        std::cout << "throwing           " << throwValue << "..." << std::endl;
        throw some_exception(throwValue);

    } catch (...) {
         currentException = std::current_exception();
    }

    return currentException;
}

void rethrowException(std::exception_ptr ePtr) {
    try {
        if (ePtr) {
            std::rethrow_exception(ePtr);
        }

    } catch (const some_exception & e) {
        std::cout << "catched int value: " << e.v << std::endl;
    }

    std::exception_ptr anotherExceptionPtr = ePtr;
    try {
        if (anotherExceptionPtr) {
            std::rethrow_exception(anotherExceptionPtr);
        }

    } catch (const some_exception & e) {
        std::cout << "catched int value: " << e.v << std::endl;
    }
}

void checkMakeExceptionPtr() {
    std::exception_ptr currentException = std::make_exception_ptr(some_exception(20));
    std::cout << "exception_ptr constructed" << std::endl;

    rethrowException(currentException);
}

void exceptionPtrSample() {
    rethrowException(throwExceptionAndCaptureExceptionPtr());
    checkMakeExceptionPtr();
}

Если мы запустим функцию exceptionPtrSample на выполнение, то увидим примерно следующий результат:
throwing 10…
int ctor
catched int value: 10
catched int value: 10
int ctor
copy ctor
copy ctor
exception_ptr constructed
catched int value: 20
catched int value: 20

Для того чтобы можно было удобно работать с exception_ptr, есть несколько вспомогательных функций:
  • current_exception - данная функция возвращает exception_ptr. Если мы находимся внутри блока catch, то возвращает exception_ptr, который содержит обрабатываемое в данный момент текущим потоком исключение, если вызывать ее вне блока catch, то она вернет пустой объект exception_ptr
  • rethrow_exception - данная функция бросает исключение, которое содержится в exception_ptr. Если входной параметр не содержит исключения (пустой объект), то результат не определен. При этом msvc кидает std::bad_exception, а программа собранная с помощью gcc-4.7.2неожиданно завершается.
  • make_exception_ptr - данная функция, может сконструировать exception_ptr без бросания исключения. Ее предназначение аналогично функции std::make_shared - конструирование объекта. Ее выполнение аналогично функции throwExceptionAndCaptureExceptionPtr.  В реализации от gcc-4.7.2 make_exception_ptr делает два копирования объекта some_exception.

Передача исключений между потоками

Как мне кажется, тип exception_ptr создавался именно для решения проблемы передачи исключений между потоками, поэтому давайте разберемся, каким образом мы можем передать исключение из одного потока в другой:
void worker(std::promise<void> & p) {
    try {
        throw std::runtime_error("exception from thread");
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

void checkThreadAndException() {
    std::promise<void> p;
    auto result = p.get_future();

    std::thread t(worker, ref(p));
    t.detach();

    try {
        result.get();
    } catch (const std::runtime_error & e) {
        std::cout << "runtime error catched from async worker" << std::endl;
    }
}

Вообще, многопоточность в C++11 - это обширная тема, там есть свои тонкости, нюансы и о них следует писать отдельно. Сейчас мы рассмотрим этот пример только ради передачи исключения. Запускаем функцию worker в отдельном потоке, и эта функция кидает исключение. Объект класса promise позволяет организовать связь между разными потоками, и атомарно передать значения из одного потока в другой (или исключение). В данном примере мы как раз пользуемся методом set_exception, который принимает exception_ptr в качестве параметра. Для того чтобы получить значение, мы создаем объект класса future- это наш result и вызываем метод get. Также необходимо у потока вызвать метод detach или join, так как при разрушении объекта t в деструкторе проверяется, чтобы joinable() == false, иначе вызывается std::terminate. Скорее всего, это связано с тем, чтобы программист не "отпускал потоки на волю", а всегда следил за ними (либо отпускал явно с помощью метода detach)

Отдельно стоит сказать об использовании многопоточности в gcc-4.7. Изначально этот пример у меня не работал (выбрасывал исключение), погуглив, я выяснил, что для использования std::thread необходимо передать линкеру флаг-pthread. Так как я использую CMake в качестве системы сборки, то эта задача упрощается (тут сложность может возникнуть при использовании gcc на разных платформах, например на sparc solaris используется флаг -thread вместо -pthread) - есть специальный CMake модуль Threads, в котором эта проблема решена:

find_package(Threads REQUIRED)
#…
target_link_libraries(cxx_exceptions ${CMAKE_THREAD_LIBS_INIT})

Nested exceptions

Как видно из названия, данный механизм позволяет "прицепить" к кидаемому исключению другие исключения (которые могли быть брошены раньше). Например, если у нас есть своя иерархия исключений, то мы можем ловить все "сторонние" исключения, прицеплять их к своим исключениям, а при обработке своих исключений мы можем выводить доп. информацию, которая к ним "прицеплена" (например, при отладке, мы можем печатать информацию о сторонних исключениях). Хороший пример использования nested exception приведен на cppreference.com, мой пример, отчасти пересекается с ним:
struct third_party_exception {
    explicit third_party_exception(int e) : code(e) {
    }

    int code;
};


void third_party_worker() {
    throw third_party_exception(100);
}

class our_exception : public std::runtime_error {
public:
    our_exception(const std::string & e) : std::runtime_error("our error: " + e) {
    }
};

void ourWorker() {
    try {
        third_party_worker();
    } catch (...) {
        throw_with_nested(our_exception("worker failed"));
    }
}

void exceptionHandler(const our_exception & e, bool catchNested) {
    std::cerr << "our exception catched: " << e.what();

    if (catchNested) {
        try {
            rethrow_if_nested(e);
        } catch (const third_party_exception & e) {
            std::cerr << ", low level reason is: " << e.code;
        }
    }

    std::cerr << std::endl;
}

void exceptionProcessing() {
    try {
        ourWorker();
    } catch (const our_exception & e) {
        exceptionHandler(e, false);
    }

    try {
        ourWorker();
    } catch (const our_exception & e) {
        exceptionHandler(e, true);
    }
}

Итак, у нас есть сторонняя функция, которая кидает исключение, мы можем написать адаптер, который ловит "сторонние" исключения, и из них делает "наше" исключение: в качестве "сторонней" функции и "нашей" функции выступают соответственно third_party_worker и ourWorker. Мы ловим все исключения, и бросаем далее уже наше (our_exception) исключение, при этом, к нему было прицеплено какое-то (мы в принципе можем и не знать какое) "стороннее" исключение. После этого работаем уже с нашими исключениями. При этом, если нам понадобится более детальная информация о том, что происходило на "нижнем" уровне, то мы всегда можем вызвать функциюrethrow_if_nested. Данная функция анализирует, если ли прицепленное (вложенное) исключение, и если есть, то бросает это вложенное исключение. Функция exceptionHandler принимает "наше" исключение и дополнительный флаг, который разрешает или запрещает вывод информации о стороннем исключении. Мы можем управлять выводом сторонних исключений, управляя параметром catchNested, например, из файла конфигурации (или в зависимости от сборки - Release, Debug).

Для работы с nested exception есть один класс и две функции:

  • nested_exception - данный класс "подмешивается" к бросаемому объекту при вызове функции std::throw_with_nested - этот класс также позволяет вернуть вложенное исключение (exception_ptr) с помощью метода nested_ptr. Также этот класс имеет метод rethrow_nested, который бросает вложенное исключение
  • throw_with_nested - данная шаблонная функция принимает объект некоторого типа (назовем его InputType), и бросает объект который является наследником std::nested_exception и нашего InputType (в реализации от gcc - это шаблонный тип, который наследуется от nested_exception и InputType). Таким образом, мы можем ловить как объект нашего типа, так и объект типа nested_exception и уже потом получать наш тип через метод nested_ptr
  • rethrow_if_nested - данная функция определяет, есть ли у объекта вложенное исключение, и если есть, то бросает его. Реализация может использовать dynamic_cast для определения наследования

В принципе, механизм nested exception довольно интересный, хотя реализация может быть довольно тривиальна, ее можно сделать самому, с помощью ранее описанных функций current_exception и rethrow_exception. В той же реализации gcc, класс nested_exception содержит одно поле типаexception_ptr, которое инициализируется в конструкторе с помощью функцииcurrent_exception, а реализация метода rethrow_nested просто вызывает функцию rethrow_exception.

Спецификация noexcept

Данный механизм можно рассматривать как расширенный (и ныне устаревший) механизм throw(). Основное его предназначение как и раньше - гарантия того что, функция не бросит исключение (если гарантия нарушается, то вызывается std::terminate).

Используется этот механизм в двух видах, аналогично старому throw()

void func() noexcept {
//...
}

И в новом виде:

void func() noexcept(boolean_expression_known_at_compile_time) {
//...
}

При этом, если значение выражения вычислено как истина, то функция помечается как noexcept, иначе, такой гарантии нет.
Также есть соответствующий оператор noexcept(expression), который тоже выполняется в compile time, этот оператор возвращает истину, если выражение не бросает исключение:
void noexceptSample() {
    cout << "noexcept int():         " << noexcept(int()) << endl;
    cout << "noexcept vector<int>(): " << noexcept(vector<int>()) << endl;
}

Данный код для gcc-4.7.2 выводит:
noexcept int(): 1
noexcept vector<int>(): 0

Здесь мы видим, что конструктор встроенного типа int не бросает исключение, а конструктор вектора может бросить (не помечен как noexcept). 
Это удобно применять в шаблонном метапрограмировании, используя данный оператор мы можем написать шаблонную функцию, которая в зависимости от параметра шаблона может быть помечена как noexcept или нет:

template <typename InputTypeT>
void func() noexcept(noexcept(InputTypeT())) {
    InputTypeT var;
    /// do smth with var
    std::cout << "func called, object size: " << sizeof(var) << std::endl;
}

void noexceptSample() {
    std::cout << "noexcept int():         " << noexcept(int()) << std::endl;
    std::cout << "noexcept vector<int>(): " << noexcept(std::vector<int>()) << std::endl << std::endl;

    /// @note function is not actually called
    std::cout << "noexcept func<int>:         " << noexcept(func<int>()) << std::endl;
    std::cout << "noexcept func<vector<int>>: " << noexcept(func<std::vector<int>>()) << std::endl;
}

Данный пример выводит:
noexcept int(): 1
noexcept vector<int>(): 0

noexcept func<int>: 1
noexcept func<vector<int>>: 0

Резюме

Стандарт C++11 привнес много нового в обработку ошибок, конечно, ключевой особенностью здесь остается exception_ptr и возможность передачи произвольных исключений как обычные объекты (в функции, передавать исключения между потоками). Раньше, в каждом потоке приходилось писать развесистый try… catch для всех исключений, а этот функционал существенно минимизирует количество try… catch кода.

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

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

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 06.12.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
VMware Horizon Apps Standard, v7 : 10 Pack (Named User)
Symantec Endpoint Protection Small Business Edition, Initial Hybrid Subscription License with Support, 1-24 Devices 1 YR
SmartBear LoadComplete - Node-Locked License Subscription w/ 250 Virtual Users (includes 1 year of Maintenance)
ABBYY FineReader 14 Standard Full
Microsoft Office 365 для Дома 32-bit/x64. 5 ПК/Mac + 5 Планшетов + 5 Телефонов. Подписка на 1 год.
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
СУБД Oracle "с нуля"
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Реестр Windows. Секреты работы на компьютере
Новые материалы
Работа в Windows и новости компании Microsoft
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100