Tuesday, October 19, 2010

Об исключениях и С++

    В С++ есть исключения и есть RAII техники. Теперь абстрактная задача:
Есть некие внешние функции Start, Stop и Task. Мы с ними работаем в таком порядке: Start вызывать нужно до вызова Task а Stop - после(причем Stop вызывать нужно обязательно, но только, если был вызван Start - строгое условие). Клиентский код мы изменить не можем.

    Теперь более подробнее - некий псевдо-код задачи:


try
{
    if (NULL != p)
    {
        p->Start(); // может бросить исключение

        // Некий опасный код бросающий исключения...

        DangerTask(); // может бросить исключение

        // Некий опасный код бросающий исключения...

        p->Stop(); // может бросить исключение
    }
    else
    {
        // Некий опасный код бросающий исключения...

        DangerTask(); // может бросить исключение, а также - повторение кода

        // Некий опасный код бросающий исключения...
    }
}
catch(const std::exception&)
{
    // И что делать? Мы должны вызывать p->Stop() или нет?
}
catch(...)
{
    // И что делать? Мы должны вызывать p->Stop() или нет?
}

    Этот псевдокод плох тем, что имеется повторение кода, неоднозначность в обработке ошибок, небезопасный код. В большинстве случаев такое прокатывает - т.к. важно чтоб заработало все побыстрее, и был низкий уровень входимости/опыта для суппорта, и да - уровень входимости ниже, но если код растет линейно - то время на его добавление экспонециально растет. Но сейчас не об этом, нужно просто написать правильный транзакционный код, и без повторений кода (см. выше - DangerTask в коде дважды вызывается).
    Это довольно типичная задача. В большинстве случаев люди на это(возможные исключения и повторения кода) не обращают внимание(т.к. они в своем коде не бросают исключения - а думают что если оно произошло - то это мол редкость и вообще уже мало что можно сделать в этом случае). Не буду говорить о вероятности и т.п. Суть в том - что нам нужно обеспечить строгую гарантию безсбойного транзакционного режима Start/Stop, и мы допускаем что чужой код и наш код весьма бросает исключения.
    Задача на самом деле не сложная. Апологеты ООП сразу вспомнят про RAII - и наплодят пару-тройку классов которые собой обернут эту семантику. В принципе - это всегда хорошее решение. Но вот мне не хочется писать каждый раз классы-обертки. Также не хочу использовать сторонние библиотеки. Для этой цели(чтоб не плодить новое) можно использовать дополнения к STL - std::tr1::shared_ptr и std::tr1::bind вот так:

try
{
    // Некий опасный код бросающий исключения...

    // Это сложная атомарная операция, которую нельзя испортить
       своим новым небезопасным кодом

    p && std::tr1::shared_ptr < void >
        ((p->Start(), LPVOID(NULL)), std::tr1::bind(&IPolicy::Stop, p)),
            DangerTask();
// может бросить исключение

    // Некий опасный код бросающий исключения...
}
catch(const std::exception&)
{
    // Делаем что что-либо, но мы уже не думаем про p->Stop()
}
catch(...)
{
    // Делаем что что-либо, но мы уже не думаем про p->Stop()
}

Также можно заюзать ninja_ptr с предыдущего поста:

try
{
    // Некий опасный код бросающий исключения...

    p && ninja_ptr < IPolicy > ((p->Start(), p), &IPolicy::Stop),
        DangerTask(); // может бросить исключение

    // Некий опасный код бросающий исключения...
}
catch(const std::exception&)
{
    // Делаем что что-либо, но мы уже не думаем про p->Stop()
}
catch(...)
{
    // Делаем что что-либо, но мы уже не думаем про p->Stop()
}

Вот такие приколы. Код я переделал с реального, но не скомпилировал, надеюсь "скобочки" все на месте.

5 comments:

  1. Это пиздец, товарищи.
    Я чуть не ебанулся, разбирая этого уродца:
    NULL != p && (std::tr1::shared_ptr < void > (
    (p->Start(), LPVOID(NULL)),
    std::tr1::bind(&IPolicy::Stop, p)), p);

    ReplyDelete
  2. Чем тебе-то иф помешал, изверг?

    ReplyDelete
  3. кстате, эта красота не работает:

    NULL != p && (std::tr1::shared_ptr < void > (
    (p->Start(), LPVOID(NULL)),
    std::tr1::bind(&IPolicy::Stop, p)), p);
    временный shared_ptr будет уничтожен на следующей точке следования. Т.е. когда "выполнится" преобразование к bool указателя p, в конце второго литерала конъюнкции.

    ReplyDelete
  4. Вместо if я использовал логическое && для обхода области видимости - что позволяет избежать дублирования DangerTask. Вот так бы было с if:

    try
    {
    if (NULL != p)
    {
    std::tr1::shared_ptr < void > (
    (p->Start(), LPVOID(NULL)),
    std::tr1::bind(&IPolicy::Stop, p));

    DangerTask(); // дублирование
    }
    else
    {
    DangerTask(); // дублирование
    }
    }
    catch(const std::exception&)
    {
    }
    catch(...)
    {
    }

    ReplyDelete
  5. Насчет "не работает". Да, увы это так. Я исправил в самом посте. Надеюсь больше ниче не забыл, ведь до этого я его не проверял..

    ЗЫ, в ninja_ptr с пред. поста есть баг

    ReplyDelete