заметки на полях
Nov. 5th, 2011 03:11 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Есть много разных причин, в силу которых использование exceptions в С++ затруднено по сравнению с иными языками, наример по сравнению с java.
Одна из них - в том, что оператор 'return' в С++ может кинуть исключение, поскольку он может вызывать конструктор копирования. Если функция, из которой брошено это исключение, меняет состояние каких-то данных, и нам необходимо обеспечить транзакционную семантику, т.е. полностью восстановить состояние этих данных, написание корректного кода в подобном случае превращается в непростую, а порой и невозможную для решения задачу и в любом случае не сопровождается кодом, лёгким для написания и понимания.
Появление в новом С++ move-конструктора не сильно улучшает положение, мне кажется, поскольку невозможно проконтролировать(?), что для произвольного класса наличествует не только copy но и move конструктор и опять же невозможно проконтролировать (?), что этот move конструктор не кидает исключений. Да даже если бы и можно было бы всё это проконтролировать, не для всякого класса nothrow move конструтор будет доступен.
В сегодняшнем (вчерашнем) С++ данная проблема привела к "более правильному" дизайну интерфейсов, например, в STL функция pop_front() класса std::list ничего не возвращает, а "извлечение" головного объекта предполагается делать в два приёма - сначала посредством вызова функции front(), возвращающей первый элемент списка по значению, а потом вызовом функции pop_front().
Увы, это решение, по-видимому, не вполне работает для мультитредных сценариев. Если для blocking имплементаций мы еще можем потребовать вызывать две функции вроде front() и pop_front() под единым мютексом, то в случае lock-free алгоритмов коду пользователя уже придется каким-то образом поставлять в функию pop_front() некий hint, посредством которого можно будет определить тот ли элемент будет удален из списка, который был ранее вычитан, или уже совсем другой. По уму, конечно, в lock-free надо сначала удалить элемент из списка, а потом вернуть его вызывающему коду, но это ровно то, с чем С++ при наличии исключений не справляется.
Существуют альтернативные решения, как например, передача [out] параметра по ссылке или указателю и, соответственно, присваивание ему и возврат void. Это, опять же, не идеально, поскольку семантика присваивания может быть для объекта не определена/запрещена. Ах да, в этом случае и семантика копирования, скорее всего, определена не будет. На помощь приходят move-конструкторы и move-присваивания нового С++, но... если они могут сами кинуть исключение, то дело плохо, задача обеспечания транзакционной семантики опять не решена.
Как last resort можно попытаться воспользоваться операцией swap, которую всякий вменяемый программист обязан имплементировать с гарантией nothrow. Во всяком случае, мы можем надеяться, что никакой чертов компилятор не сгенерит эту функцию автоматически не так как нам надо, и если уж она не написана для какого-то класса, то мы это увидим на этапе компиляции. Ну а если она для какого-то класса не написана, то мы всегда можем завернуть этот класс в Handle, который будет на него ссылаться через указатель (hello, java world!), и в котором, соответственно, можно легко самому имплементировать операцию swap.
Возможно, всё это можно завернуть в подходящие темплейты, заворачивающие классы в Handles автоматически, при налиии необходимости. Также возможно, что подобные решения можно задействовать автоматически для работы не со swap, а с move constructors/assignments, при отсутствии nothrow move constructors & move assignments в базовых классах, классах-аргументах итд итп.
В целом, всё выглядит довольно неуклюже и сложно, как это, собственно, и принято в этом языке.
----------
В этой заметке возможны ошибки, связанные с тем, что автор достаточно приблизительно представляет себе множество частных решений, принятых в стандарте С++ относительно move-конструкторов и move-присваиваний.
Одна из них - в том, что оператор 'return' в С++ может кинуть исключение, поскольку он может вызывать конструктор копирования. Если функция, из которой брошено это исключение, меняет состояние каких-то данных, и нам необходимо обеспечить транзакционную семантику, т.е. полностью восстановить состояние этих данных, написание корректного кода в подобном случае превращается в непростую, а порой и невозможную для решения задачу и в любом случае не сопровождается кодом, лёгким для написания и понимания.
Появление в новом С++ move-конструктора не сильно улучшает положение, мне кажется, поскольку невозможно проконтролировать(?), что для произвольного класса наличествует не только copy но и move конструктор и опять же невозможно проконтролировать (?), что этот move конструктор не кидает исключений. Да даже если бы и можно было бы всё это проконтролировать, не для всякого класса nothrow move конструтор будет доступен.
В сегодняшнем (вчерашнем) С++ данная проблема привела к "более правильному" дизайну интерфейсов, например, в STL функция pop_front() класса std::list ничего не возвращает, а "извлечение" головного объекта предполагается делать в два приёма - сначала посредством вызова функции front(), возвращающей первый элемент списка по значению, а потом вызовом функции pop_front().
Увы, это решение, по-видимому, не вполне работает для мультитредных сценариев. Если для blocking имплементаций мы еще можем потребовать вызывать две функции вроде front() и pop_front() под единым мютексом, то в случае lock-free алгоритмов коду пользователя уже придется каким-то образом поставлять в функию pop_front() некий hint, посредством которого можно будет определить тот ли элемент будет удален из списка, который был ранее вычитан, или уже совсем другой. По уму, конечно, в lock-free надо сначала удалить элемент из списка, а потом вернуть его вызывающему коду, но это ровно то, с чем С++ при наличии исключений не справляется.
Существуют альтернативные решения, как например, передача [out] параметра по ссылке или указателю и, соответственно, присваивание ему и возврат void. Это, опять же, не идеально, поскольку семантика присваивания может быть для объекта не определена/запрещена. Ах да, в этом случае и семантика копирования, скорее всего, определена не будет. На помощь приходят move-конструкторы и move-присваивания нового С++, но... если они могут сами кинуть исключение, то дело плохо, задача обеспечания транзакционной семантики опять не решена.
Как last resort можно попытаться воспользоваться операцией swap, которую всякий вменяемый программист обязан имплементировать с гарантией nothrow. Во всяком случае, мы можем надеяться, что никакой чертов компилятор не сгенерит эту функцию автоматически не так как нам надо, и если уж она не написана для какого-то класса, то мы это увидим на этапе компиляции. Ну а если она для какого-то класса не написана, то мы всегда можем завернуть этот класс в Handle, который будет на него ссылаться через указатель (hello, java world!), и в котором, соответственно, можно легко самому имплементировать операцию swap.
Возможно, всё это можно завернуть в подходящие темплейты, заворачивающие классы в Handles автоматически, при налиии необходимости. Также возможно, что подобные решения можно задействовать автоматически для работы не со swap, а с move constructors/assignments, при отсутствии nothrow move constructors & move assignments в базовых классах, классах-аргументах итд итп.
В целом, всё выглядит довольно неуклюже и сложно, как это, собственно, и принято в этом языке.
----------
В этой заметке возможны ошибки, связанные с тем, что автор достаточно приблизительно представляет себе множество частных решений, принятых в стандарте С++ относительно move-конструкторов и move-присваиваний.