В своих прошлых докладах (http://cpp-russia.ru/?p=198, и http://cpp-russia.ru/?page_id=1239) я рассказывал о C++ без исключений, как с эти жить, как работать. Этот доклад является продолжением этой серии. Я рекомендую освежить в памяти предыдущие доклады, чтобы наша работа была более продуктивной. Мы обсудим механизмы создания, копирования и перемещения объектов, механизмы аллокации и деаллокации памяти, а также обработку ошибок и исключений. Также мы обсудим проблемы и неудобства, которые испытывает программист, когда пишет код без исключений. В конце, я попытаюсь показать, как можно проектировать структуры данных, контейнеры для удобной работы в средах с исключениями и без исключений.
4. C++ БЕЗ ИСКЛЮЧЕНИЙ
try
{
throw std::logic_error("");
}
catch (...)
{
// handle error
}
error C2980: C++ exception handling is not supported with /kernel
11. ОГРАНИЧЕНИЯ
Есть исключения Нет исключений
Операция кидает исключение Операция возвращает ошибку
Конструктор копирования Двухфазная инициализация
Оператор присваивания Явный метод копирования
19. CURRENT
noexcept (part of function type since C++-17)
std::move
std::is_nothrow_constructible (default, copy, move)
std::is_nothrow_assignable (copy, move)
std::is_nothrow_swappable (since C++-17)
26. C++ WITH EXCEPTIONS
template <typename T>
struct allocator {
T* allocate(size_t count); // only if has exceptions
void deallocate(T* location, size_t count); // only if has exceptions
T* allocate_nothrow(size_t count) noexcept;
void deallocate_nothrow(T* location, size_t count) noexcept;
};
template <typename Allocator>
struct allocator_traits {
static pointer allocate(Allocator& a, size_t count); // only if has exceptions
static pointer allocate_nothrow(Allocator& a, size_t count) noexcept;
};
Добрый день, меня зовут Алексей Кутумов, я являюсь старшим разработчиком в ЛК
Итак, вот примерный план моего рассказа.
Что вообще это за зверь?
Ну вообще-то, ничего сложного здесь нет. Просто означает, что мы не имеем права или не можем использовать некоторые конструкции языка.
При этом компилятор, может нам помогать, например, вот так, как показано на слайде.
Заметьте, объектами класса std::exception и его наследниками можно пользоваться. Но по факту они становятся бесполезны, так что про них забываем.
Зачем вообще нужен C++ без исключений?
Первое, что приходит на ум – системное программирование. Всякие драйвера для ОС. Например, виндовый компилятор запрещает использование исключений в коде драйверов, я показывал ошибку на предыдущем слайде. Но, при этом, он не запрещает использование C++!
Также еще, game development, еще несколько лет назад для консолей компиляторы не имели поддержки исключений.
Аналогично, требования к высокопрозводительным системам, иногда не позволяют использовать исключения. Особенно раньше, когда исключения были дорогими. К тому же, когда говорят, что исключения не влияют на производительность, на самом деле лукавят. Имеется ввиду, что основной путь в программе не проседает по производительности, но если мы решим бросить исключение, то здесь нас ждет сюрприз.
Например, в винде, чтобы бросить исключение реализация C++ рантайма вызывает функцию __CxxThrowException, которая в свою очередь зовет системную функцию RaiseException, которая уходит в ядро. Понятно, что просто вернуть код ошибки в этом случае будет дешевле.
Если кому-то интересно, я после доклада могу показать дизасм этих функций, чтобы убедиться наглядно.
Кроме всего прочего, есть всякие coding convention, ограничения среды. Например, кидать исключения из динамических библиотек очень плохо, потому-что могут быть различия в рантайме приложения и самой библиотеки. На самом деле, в принципе, любому C++ объекту лучше не пересекать границы модулей, как раз из-за различия в рантайме, но об этом итак много где пишут и говорят.
Если взять тот-же google, то у них тоже запрещены исключения, но по-другому – там throw – это std::abort. Такой подход, используется, например, в хромиуме.
Вот мы потихоньку подходим к интересным вещам.
Какие есть ограничения в C++ без исключений? Вопрос к залу.
Какие есть проблемы в этом коде в среде без исключений?
Ну, мы же здесь выделяем память. Что будет, если мы не сможем выделить память под нашу строку, ну, в обычном C++ у нас будет исключение. В среде без исключений этот код либо не скомпилируется, либо скорет ошибку аллокации памяти, что еще хуже.
На самом деле, в прошлых докладах, посвященных теме C++ без исключений я показывал, что стандартная библиотека C++ по большей части не пригодна к использованию в такой среде. И я предлагал варианты, как можно задизайнить библиотеку для того, чтобы она могла работать в среде без исключений.
Давайте пофантазируем. Например, наша библиотека может предоставлять вот такой конструктор, с error_condition. И мы бы смогли обработать ошибку аллокации.
Но всегда ли этот подход нас спасет?
У нас есть еще конструкторы копирования и операторы присваивания. С ними проблема заключается в том, что мы не можем поменять их сигнатуру. Если мы в конструктор копирования добавим еще аргумент, то он перестанет быть конструктором копирования, и будет каким-то другим конструктором. И при этом, компилятор попытается сгенерировать дефолтный конструктор копирования. То есть, то что раньше работало нам во благо, теперь оборачивается против нас.
С оператором присваивания ситуация несколько иная, компилятор вообще нам запретит добавить еще один параметр в наш оператор.
Так что все вот так плохо.
Вместо этого, мы можем использовать например, вот такой метод assign_copy, который возвращает ошибку.
Опять же, повторюсь. Мы сейчас с вами фантазируем, этого нет в стандартной библиотеке. Мы рассматриваем варианты, как это может быть реализовано.
Здесь в таблице я привел способы преодоления этих ограничений.
Обратите внимание, здесь мы говорим только об операциях, которые раньше могли бросить исключения.
Кто может мне назвать теперь одну большую архитектурную проблему C++ без исключений?
Озвученные ранее ограничения порождают проблемы, вот мы сейчас о них поговорим
Есть самая серьезная проблема C++ без исключений, и с ней мы ничего не можем поделать.
Навскидку, пример. Например, у вас есть класс, который содержит членом объект – ну, например, коннект к базе данных. В случае наличия исключений, вы в конструкторе своего класса сразу создаете коннект к БД, и если не получилось, то кидаете исключение. У вас получается инвариант – есть ваш объект жив, то коннект к БД создан.
В случае отсутствия исключений, вы такой инвариант не сможете сделать никогда, ну просто никогда и никак. У вас нет механизма, чтобы запретить создание объекта, если какое-то условие нарушено.
Вам придется всегда перед использованием вашего коннекта к БД проверять его на валидность (или сам объект должен это делать).
Это действительно проблема, и решать ее приходится различными средствами – как средствами языка, выдумывать какие-то конструкции, макросы, которые делают двухфазную инициализацию, использовать фабрики объектов, так и сторонними средствами – например, кодогенерация, тестирование, ревью, использование статических анализаторов.
Кстати, к чести последних, они могут очень хорошо осуществлять такие проверки, мы их используем, и они реально находят нам такие баги. Поэтому я всячески рекомендую их юзать.
Вот пример, который демонстрирует мою мысль.
Из приведенной ранее таблицы видно, что C++ без исключений вынуждает программиста писать больше кода, причем, по большей части код этот довольно противный, повторяющийся – код обработки ошибок, реализация методов assign_copy.
Больше кода – больше багов.
Как вы все знаете, компилятор генерирует довольно много кода – раскрутка стека при выходе из скоупа. Реализация стандартных операторов, конструкторов и деструкторов.
Так вот в случае отсутствия исключений, программисту приходится выполнять работу компилятора.
Вот всё оставшееся время мы будем бороться с этой проблемой.
Давайте начнем с простых и наивных подходов – макросы.
Вот мы попробовали частично решить эту проблему, сделали макрос, который позволяет нам минимизировать часть кода по проверке ошибки.
Ну да, стало немного лучше.
Здесь мы должны явно написать метод assign_copy, и еще не должны в нем ошибиться.
Заметьте, что здесь компилятор нам ничем помочь не может, мы вынуждены делать эту работу за компилятор.
Давайте подумаем, сможем ли мы как-то помочь компилятору в этом случае?
Давайте посмотрим в будущее и поймем, что нам сможет помочь.
На самом, деле, я очень жду compile time рефлексии, нам Антон Бикинеев про нее вчера рассказывал. Рефлексия поможет нам генерировать общий код. Например, мы сможем генерировать методы assign_copy автоматически для наших типов.
На текущий момент для C++14 есть еще magic_get Антона Полухина, но он имеет много ограничений и нюансов. Возможно, magic_get и C++17 помогут нам облегчить наши страдания, пока не будет готова рефлексия. К сожалению Visual Studio 2015 не поддерживается библиотекой magic_get, возможно будущая 2017 студия будет поддерживаться.
Теперь давайте обратимся к нашему компилятору и стандартной библиотеке.
Хотя я говорил раньше, что большая часть стандартной библиотеки не работает в среде без исключений, тем не менее есть довольно полезные ее части – это type_traits.
Итак, сначала давайте посмотрим на noexcept. Во-первых, это оператор, который в compile time выдает значение. Во-вторых, это спецификатор функции. Более того, с C++-17 этот спецификатор функции будет частью типа функции.
Затем, move семантика. В среде без исключений ценности move семантики существенно возрастает. Более того, обычно мув конструктор и мув присваивание легко реализовать как noexcept. В общем это однозначный must-have. К тому же, в этом случае, компилятор играет за нас. Он легко и правильно геренирует нужные нам операторы и конструкторы. Еще раз повторюсь – это must have.
Дальше – это type_traits – которые позволяют нам узнать, что объекты можно конструировать безопасно, копировать и присваивать. На самом деле, эти type_traits больше нужны разработчику библиотеку, нежели обычному программисту, мы позже посмотрим, как они нам пригодятся.
Чем нам это может помочь, ну это же очевидно, если у нас есть безопасный оператор присваивания или конструктор копирования, то нам не нужно писать методы assign_copy, выполнять двухфазную инициализацию, у нас будут более строгие инварианты класса. В общем от noexcept и is_nothrow одни сплошные плюсы.
Вот теперь, давайте поймем, как нам это может пригодиться.
Небольшая ремарка про noexcept.
Самое полезное в noexcept – это то, свойство noexcept наследуется при автогенерации операторов и конструкторов.
Например, у нас есть нетривиальная реализация дефолтного конструктора Test1, если мы ее пометим noexcept, то и автосгенерённый конструктор Test2, тоже будет noexcept. Это очень важное и полезное свойство этого спецификатора.
Почему же noexcept так важен в среде без исключений? Ну, вообще-то noexcept говорит о том, что ни при каких условиях исключение не вылетит из этой функции. А у нас здесь нет никаких исключений, получается он бессмысленный? Не совсем. Сейчас мы притянем noexcept к нашей проблеме.
Все гуру C++ в один голос говорят, не меняйте семантику стандартных операторов и конструкторов, иначе, поведение ваших объектов в стандартной библиотеке будет очень странным.
Например, оператор присваивания, в обычной среде он должен скопировать объект, или бросить исключение, если объект не скопирован. Теперь, если мы навешиваем noexcept, то мы как-бы говорим, что наша операция никогда не кинет исключение. А если вспомнить про семантику этого оператора, то это означает, что объект всегда успешно копируется, что нам и надо.
И еще один момент. Вообще-говоря, если у вас код написан для среды без исключений, а вы компилируете его в среде с исключениями, то он не должен менять своего поведения. Согласитесь, будет очень неожиданно получить разное поведение одного и того-же кода. Вот как раз noexcept и является таким защитником
Сейчас мы с вами говорили только про пользовательские типы. Но у нас же есть еще и разные контейнеры – векторы, списки, строки, и т.д.
Поэтому важно рассмотреть их тоже.
Вот здесь пример. У нас есть наша структура Message, мы делаем вектор этих структур и добавляем один элемент в конец.
Вот я говорил, что type_traits нужны разработчику библиотеки, сейчас мы поймем для чего.
Итак, кто может схематично рассказать, парой слов, как реализован метод push_back у стандартного вектора?
Здесь у меня push_back_nothrow – по сути тоже самое.
Я уже рассказывал про дизайн стандартной библиотеки для C++ без исключений, поэтому я не буду повторять этот доклад.
Я всего лишь схематично покажу реализацию метода push_back_nothrow
Итак, нам нужно реаллоцировать вектор, если это нужно. За это отвечает метод reallocate_nothrow.
А затем, нам надо вставить новый элемент в конец используя операцию копирования.
И после того, как все будет успешно, сдвигаем указатель и регистрируем наш объект в векторе.
А вот как может выглядеть реализация операции копирования объекта.
У нас есть область под объект. И в случае, если конструктор копирования noexcept, то используем его, в противном случае – зовем метод assign_copy.
Тут на самом деле я забыл еще один момент, подсказка есть на слайде, кто его найдет?
По хорошему, мне нужно проверить, что дефолтный конструктор тоже noexcept, иначе у нас могут быть проблемы.
Смотрите, две эти функции обеспечивают копирование объекта, причем заметьте, что для noexcept конструктора все вообще тривиально.
И мы не заставляем пользователя писать ненужный код метода assign_copy. Меньше кода – меньше багов )).
Теперь давайте посмотрим на следующий пример:
У нас есть некая библиотека, которая написана с учетом отсутствия исключений, и интерфейс этой библиотеки использует нашу структуру Message. Эта библиотека компилируется под три разных режима, при этом, для юзермодного приложения, она компилируется с исключениями, хоть и написана без них.
В случае, когда эта библиотека используется в ядре и в EFI драйвере – тут вопросов нет, так же как нет и исключений.
Но программисту юзермодного приложения теперь становится обидно. У меня есть исключения, почему же теперь я должен страдать, и использовать этот макрос TRY, std::error_condition.
Вопрос вполне резонный. Если бы наш nestl::vector и nestl::string умели предоставлять требуемые операции в среде с исключениями, то юзермодному программисту было бы легко использовать эти вещи, и он даже бы не задумывался, об этих макросах и связанных с ними неудобствах.
Вот наша структура, причем заметьте, если все члены этой структуры предоставляют нужные конструкторы и операторы, то компилятор сам сгенерирует их и для Message, а если их нет, то компилятор и не будет ничего делать.
Значит, нам надо каким-то образом разрешить нужные операции для наших контейнеров, когда это можно сделать безопасно, и запретить их, если операции не имеют смысла.
Итак, самый простой вариант обернуть эти методы в макрос. Так можно сделать, но мне много макросов не нравится.
Можно сделать, чтобы в зависимости от среды
Итак, вот давайте рассмотрим такой аллокатор.
Отличие его от стандартного аллокатора заключается в том, что может быть реализована либо первая пара методов, либо вторая пара методов.
Стандарт требует, чтобы была всегда первая пара методов.
В зависимости от наличия исключений, реализации, могут быть реализованы и все 4 метода, а может только 2.
И есть аллокатор трейтс, который расширяет возможности аллокатора. Этот трейт может реализовать недостающие методы (выразив один через другой).
Заметьте, что первый статический метод этот трейт определит, только если есть исключения.
То есть в среде без исключений, этот трейт будет предоставлять только второй метод.
Как выразить одну реализацию через другую, я думаю проблем с этим нет.
Чем данный трейт хорош, а тем, что он может адаптировать существующий std::allocator для наших контейнеров в среде с исключениями.
По понятным причинам мы не можем юзать std::allocator в среде без исключений.
Итак, мы делаем одну базовую реализацию, которая не зависит от исключений. Туда входит работа с внутренним представлением вектора, а также публичные методы.
После этого, делаем реализацию вектора без исключений.
И затем, расширяем реализацию, добавляя поддержку исключений.
Обратите внимание, это еще не наш вектор, у них немного другие имена.
Можно сделать немного по другому. Добавить еще один шаблонный шаблонный параметр. И тогда наши vector_ и vector_nx будут по сути обычными декораторами, и их можно будет применять к vector_base независимо.
Ну и напоследок, мы собираем наш новый вектор. Я использовал первый вариант, когда vector_x расширяет vector_nx.
Я ввожу новый trait, который позволяет узнать о наличии или отсутствии исключений.
И конкретную реализацию вектора выбирать уже исходя из этого значения.
Соответственно, vector_x и vector_nx - это две реализации вектора для разных сред.
Заметьте, я не ввожу новый тип vector, вместо этого использую using, кто может сказать для чего я это делаю?
Плюсы такого подхода заключаются в том, что vector будет иметь разные типы в среде с исключениями и в среде без исключений – это нам не даст сделать ошибки в правиле One Definition Rule, в случае, если мы случайно смешаем два объектника с разными значениями NESTL_HAS_EXCEPTIONS.