суббота, 5 сентября 2009 г.

[comp.prog.cpp] Спецификатор nothow в C++ -- имеет ли это смысл?

Из обсуждения заметки о простоте транзакционного программирования:

jazzer:
Я только добавлю, что у меня всегда в середине таких функций стоит строчка с комментарием
/// no throw after this point

Rubanets Myroslav:
Ну и ценность комментария
/// no throw after this point
у меня вызывает большие сомнения.

jazzer:
насчет ценности комментария - я не знаю другого способа, к сожалению.
Было бы замечательно, если бы можно было объявить блок как nothrow (через атрибуты, скажем), и чтоб компилятор проверял, что я зову только функции, которые тоже nothrow (проверки примерно как const), но такого, увы, нету и не предвидится.

Это обсуждение напомнило мне, что мы с jazzer-ом когда-то на RSDN уже затрагивали тему спецификатора nothrow. Насколько я знаю, он существует сейчас только в одном языке -- D. Да и то, изначально он был просто зарезервированным ключевым словом и никак не обрабатывался компилятором (может сейчас что-то и изменилось, не знаю).

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

// Реализация оператора копирования для собственного класса.
my_class_t & my_class_t::operator=( const my_class_t & other )
  {
    my_class_t tmp( other );
    // Вот здесь мы гарантируем, что исключений быть не должно.
    nothrow {
      tmp.swap( *this );
    }
  } 

Смысл в том, чтобы компилятор бил по рукам, если в секции nothrow программист пытается выполнять какой-то код, который может порождать исключения. Например, вызывать new. Т.е. программист может использовать внутри nothrow только очень ограниченный набор встроенных инструкций и функции, помеченные спецификатором nothrow. Скажем, функции swap:

// Я уверен, что моя реализация swap не бросает исключений.
void my_class_t::swap( my_class_t & o ) nothrow {
  std::swap( m_a, o.m_a );
  ...
  std::swap( m_b, o.m_b );
} 

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

Во-первых, сразу возникает вопрос: "А можно ли помечать спецификатором nothrow шаблонный код?". Взять, к примеру, тот же std::swap. Ведь он работает по очень простой схеме -- несколько копирований через временную переменную. Операторы копирования могут порождать исключения. Значит, обобщенная (т.е. исходная, шаблонная) версия std::swap не может быть помечена как nothrow. А вот ее конкретные версии-специализации для примитивных типов -- могут. Т.о. получается, что в случае с шаблонами обобщенная версия может не иметь спецификатора nothrow, а специализации -- могут. И наоборот. Получается, что для шаблонов какое-то решение существует. Может некрасивое, но все же.

Во-вторых, существует вопрос о том, насколько nothrow соотносится с системно-зависимыми вещами. Допустим, у нас есть некоторый код:

// Некоторый тип, который мы используем.
struct persistent_t : public ... {
  // Значение этого поля нам потребуется.
  int m_value; 
  ...
};

// Наша операция swap, которая не бросает исключений.
void my_class_t::swap( my_class_t & o ) nothrow {
  ...
}

// Какой-то код, в котором мы хотим получить от компилятора
// контроль за гарантиями безопасности исключений.
void resource_user_t::lock_resource(...) {
  // Вначале захватываем все нужные нам ресурсы.
  persistent_t * p = ...;
  my_class_t tmp_resource = ...;

  // А теперь остается сохранить все это у себя.
  // Но процесс сохранения не должен бросать исключений.
  nothrow {
    std::swap( m_value, p->m_value );
    m_resource.swap( tmp_resource );
  }
} 

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

В первой строке мы делаем swap для двух целочисленных переменных. Откуда здесь взяться проблемам? От хитрых баз данных :) Если мне не изменять память, первые объектно-ориентированные БД, типа Versant и Objectivity, работали через механизм захвата страниц виртуальной памяти. Т.е. когда вы создаете объект в БД, вы получаете на него указатель в своей виртуальной памяти. Но самого объекта в памяти нет. При первом обращении по этому указателю происходит системное исключение, которое перехватывается ран-таймом БД. Он определяет, что нужно загрузить и загружает объект в память. В нашем примере указатель p может как раз указывать на объект в подобной объектной БД. И когда мы попытаемся выполнить swap БД попробует поднять его содержимое из БД. Но это не обязательно увенчается успехом. Результатом чего будет какое-то исключение. В блоке, помеченном, как nothrow.

Во второй строке мы вызываем нашу собственную функцию swap, которая, казалось бы, ничего криминального не делает. Но она может просто не вызваться из-за переполнения стека! Т.е. из-за возникновения системного исключения.

Конечно, в этих случаях будут происходить события, выходящие за юрисдикцию C++ (равно как и в случае с деления на ноль). Но ведь в некоторых ОС восстановление после подобных системных исключений возможно (если мне не обманывает мой склероз, в Windows это делается посредством механизма SEH -- Structured Exception Handling). Т.е., если нам сильно не повезет, то у нас в программе может выжить объект, работа которого была прервана внутри секции nothrow.

Такие вот дела. Наверное, если бы в C++ был nothrow, который не работал бы в экзотических случаях, но зато бил бы разработчиков по рукам в остальных ситуациях, это было бы хорошо. Но, как сказал jazzer, в обозримом будущем в C++ его все равно не планируется. Так что будем посмотреть, что получится у разработчиков D. И получится ли вообще ;)

Отправить комментарий