diff --git a/Exception_and_RAII/Exception-safety.tex b/Exception_and_RAII/Exception-safety.tex new file mode 100644 index 0000000..43e219a --- /dev/null +++ b/Exception_and_RAII/Exception-safety.tex @@ -0,0 +1,126 @@ +\section{Exception safety} +\subsection{Мотивирующий пример} +Рассмотрим оператор копирования для \mintinline{c++}{string}. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +string& string::operator=(string const &other) { + size = other.size; + delete[] data; + data = new char[size]; + memcpy(data, other.data, sizeof(other.data); + return *this; +} +\end{minted} + +На первый взгляд это довольно безобидный код. Но он совершено не учитывает возможность возникновения исключения при выделении памяти. Что произойдет в этом случаи? + +Мы не ловим исключение, поэтому оно пролетит дальше. Предположим, что его кто-то поймал. Тогда он обнаружит, что наша строка в сломанном состоянии и он не может ничего с ней сделать. Любой вызов public-метода вызывает undefined behavior, так как нарушились инварианты класса: data указывает на удаленную память, а size при этом имеет не нулевое значение. + +Напишем аккуратнее: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +string& string::operator=(string const &other) { + char *buf = new char[other.size]; + memcpy(buf, other,data, sizeof(other.data); + size = other.size; + delete[] data; + data = temp; + return *this; +} +\end{minted} + +Если исключения не возникнет, то функция скопирует данные, иначе объект останется неизменным. Это позволяет пользователю класса корректно обрабатывать ошибки, используя public-методы класса, так как инварианты выполняются. Также важно, что теперь при возникновении исключения данные не теряются. + +Давайте обобщим этот пример: + +Часто мы хотим, чтобы в случае возникновения исключение в коде какой-то структуры данных инварианты класса сохранились, чтобы пользователь мог корректно обработать вылетевшие исключения. Также желательно, чтобы состояние объекта не изменилось. + +Это идея развилась в Гарантии безопасности исключений. + +\subsection{Определение} + +Гарантии безопасности исключений (\textit{англ.} Exception safety) -- это контракт для методов класса относительно исключений. + +Уровни гарантий: +\begin{enumerate} +\item \textbf{<>} -- Гарантируется, что инварианты класса сохраняются и не происходит утечек памяти или других ресурсов. +\item \textbf{<>} -- Включает в себя базовую гарантию. А также требует, что в случае исключения объект остается в том, состоянии, в котором он был до выполнения операции. То есть либо операция прошла успешно, или она не повлияла на объект. +\item \textbf{<>} -- Кроме базовой гарантии, гарантируется, что исключения не возникают. +\item \textbf{<>} -- нет ни каких гарантий. После выполнения метода объект и данные в нем могут быть в любом состоянии. Предполагается, что продолжать работу программы нельзя. +\end{enumerate} + +\textcolor{red}{NB}) Отдельно стоит сказать про гарантии безопасности конструкторов/деструкторов, так как до/после их вызова объекта не существует. Поэтому выделим для них только два вида гарантии: \textbf{Nothrow} и \textbf{Strong}. + +\textcolor{red}{NB}) Определения можно аналогично использовать не только для описания состояния объектов некоторого класса, но и для описания состояния программы в целом. + +Например, строгая гарантия говорит, что либо функция выполнилась успешно, либо состояние программы не изменилось. + + + +Разберем пару примеров. +\begin{itemize} +\item \mintinline{c++}{std::swap} -- имеет гарантию \textit{Nothrow}. То есть при любых обстоятельствах нам гарантируется, что функция отработает корректно. + +Также этой гарантии отвечает \mintinline{c++}{pop_back()}. + +\item \mintinline{c++}{operator=} -- нелья сделать \textit{Nothrow}, так необходимо выделить память. Но мы можем сделать Strong гарантию, если скопируем данные в временный объект, а потом сделать \mintinline{c++}{swap()}, если все прошло успешно. Иначе временный объект удалиться и \mintinline{c++}{this} не измениться. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +vector& vector::operator=(vector const& other) { + return this->swap(vector(other)); // swap trick! +} +\end{minted} +Такой оператор копирования будет отвечать строгой гарантии, если \mintinline{c++}{std::swap} не будет бросать и конструктор будет отвечать хотя бы строгой гарантии. + +\textcolor{red}{NB}) Этот метод называется swap trick. Его суть заключается в том, что мы делаем операции отвечающие хотя бы базовой гарантии во временном объекте. После чего заменяем им наш текущий объект. Если исключение произойдет до замены, то изначальное состояние объекта не потеряется. + +\item +Всегда ли можно предоставить строгую гаратию? +Рассмотрим следующий код: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void g() { + f(); +} +\end{minted} +Необходимо предоставить для функции \mintinline{c++}{g()} строгую гарантию. Если функция \mintinline{c++}{f()} отвечает только базовой гарантии, то необходимо запомнить перед ее вызовом состояние программы. Не всегда достаточно запомнить только копию объекта. Если \mintinline{c++}{f()} именяет глобальные данные, то придется запомнить и их состояние. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void g() { + // делаем копию данных + try { + f(); + } catch (...) { + // восстановить состояние. + throw; // пробросить + } +} +\end{minted} +Тут может быть несколько проблем. Во-первых делать копию состояния может быть дорого. Во-вторых это может быть просто не возможно. Например, если изменилось состояние базы данных, то откатить его нет возможности. + +Поэтому иногда предоставить базовую гарантию -- лучшее решение. +\end{itemize} + +\subsection{Best practice} +\begin{itemize} + +\item +Старайтесь предоставлять самую сильную гарантию, где это оправдано. +Как минимум, методы пользовательского интерфейса должны удовлетворять хотя бы какой-то гарантии. Это избавляет пользователей от утечек памяти и инвалидных данных. + +\item + По возможности деструкторы должны отвечать гарантии Nothrow, так как они могут быть вызваны по время раскрутки стека. И если в это время произойдет исключение, то программа будет завершена функцией \mintinline{c++}{std::terminate()}. + + Начиная с c++11 все деструкторы неявно помечены как \mintinline{c++}{noexcept}. + +\item Используйте swap trick. + +\item Спецификатор \mintinline{c++}{noexcept} (C++11) указывает компилятору, что выполняется гарантия nothrow. + +Это важная информация для компилятора, которая позволят делать некоторые оптимизации кода связанные с проверкой исключений. + +Также это может быть важно при использовании STL. Так как при перемещении бывает сложно обрабатывать исключения, то стандартные алгоритмы могут игнорировать ваш конструктор перемещения если он не помечен как \mintinline{c++}{noexcept}. + +\item +Главным способом предотвращения утечек памяти и других ресурсов является идиома RAII-классов (об этом подробнее ниже). + +\end{itemize} +\textcolor{red}{Offtop:} + +Можно попросить оператор \mintinline{c++}{new} не кидать исключение с помощью константы \mintinline{c++}{std::nothrow} diff --git a/Exception_and_RAII/Exception.tex b/Exception_and_RAII/Exception.tex new file mode 100644 index 0000000..a17b73d --- /dev/null +++ b/Exception_and_RAII/Exception.tex @@ -0,0 +1,368 @@ +\section{Exceptions} +\subsection{Введение} +Часто нехватка динамической памяти, неправильный ввод пользователя, ошибка с файловой системой, приводят к тому, что продолжение исполнения логики программы невозможно. Например, если наша функция foo вызывает malloc и malloc вернул ошибку, то функция foo должна завершиться и тоже вернуть ошибку. Возможно, что функция, вызывающая функцию foo, тоже проверит возвращаемое значение и завершится с ошибкой. + +В C такая поведение реализовывалось, явной проверкой возвращаемого значения функции с помощью if и исполнением return'а в случае ошибки. C++ имеет встроенный в язык механизм поддержки такого поведения. Этот механизм называется механизмом исключений. + +Рассмотрим следующую функцию деления: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void div(int a, int b) { + return a / b; +} +\end{minted} + +Если в эту функцию передать в качестве $b$ $0$, то произойдет undefined behavior. Предположим, что мы хотим, чтобы функция сообщала об ошибке, когда $b = 0$. Для этого сначала необходимо объявить класс исключения, объекты которого будут хранить в себе информацию об исключении. + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class Division_by_zero { + int dividend + string message; + Division_by_zero(int dividend, string const &message) : + dividend(dividend), message(message) { } +}; +\end{minted} + +Теперь можно переписать функцию $div$ следующим образом: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void div(int a, int b) { + if (b == 0) // возникает исключительное состояние + throw Division_by_zero(a, "in function div(int, int)"); // генерируем исключение. + return a / b; +} +\end{minted} + +Оператор throw завершает исполнение текущей функции и возвращает ошибку в вызывающую функцию. Вызывающая сторона может обработать исключение следующим образом: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int main() { + int n; + cin >> n; + try { // здесь указываем операторы, в которых мы хотим ловить исключения. + for (int i = 0, a, b; i < n; ++i) { + cin >> a >> b; + cout << div(a, b); + } + } catch(Division_by_zero const& obj) { //здесь указываем тип исключения, которое мы хотим обработать + // здесь обрабатываем исключение + cout << obj.dividend << "div by 0 " << obj.message(); + } +} +\end{minted} + +В данном примере, при завершении div с исключением, цикл for прерывается и исполняется catch-блок, который выводит сообщение об ошибке. После чего функция main завершается. + +Если исключения не возникает, то цикл for отработает до конца, catch-блок вызван не будет. + +\subsection{Описание конструкций} +Рассмотрим используемые конструкции подробнее. Блок \mintinline{c++}{try-catch} используется для обработки исключений и имеет общий вид: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +try { /*операторы защищенного блока*/ } +// catch-блоки +catch(exception1 const& e) {/*код обработки*/} +catch(exception2 const& e) {/*код обработки*/} +... +catch(exceptionN const& e) {/*код обработки*/} + +\end{minted} +Внутри блока try пишется код, исключения в котором необходимо ловить и обрабатывать. Если изнутри блока try вылетит исключение то, компилятор попытается подобрать подходящий catch-блок. В catch блоке указывается какие действия необходимо сделать, при возникновении исключения указанного типа. + +Catch-блок может иметь две формы: +\begin{itemize} + \item + \mintinline{c++}{catch(/*declaration*/) { /*обработчик исключения*/ }} Ловит исключение указанного или производных от него типов. Переменной исключения можно дать имя. Это позволяет обращаться к пойманному объекту исключения. + \item + \mintinline{c++}{catch(...) { /*обработчик исключения*/ }} + Ловит исключения всех типов. В этом случае обращаться к объекту исключения невозможно. +\end{itemize} + +Оператор \mintinline{c++}{throw} генерирует исключение. (Иногда говорят, "бросает"\ или "выбрасывает"\ исключение). При генерации исключения происходит следующее: +\begin{enumerate} + \item + Создается копия объекта переданного в оператор throw. Эта копия будет существовать до тех пор, пока исключение не будет обработано. Если тип объекта имеет конструктор копирования, то для создания копии будет использован конструктор копирования. + \item + Прерывается исполнение программы. + \item + Выполняется раскрутка стека, пока исключение не будет обработано. +\end{enumerate} + +При раскрутке стека, вызываются деструкторы локальных переменных в обратном порядке их объявления. После разрушения всех локальных объектов текущей функции процесс продолжается в вызывающей функции. Раскрутка стека продолжается пока не будет найден try-catch-блок. При нахождении try-catch-блока, проверяется, может ли исключение быть обработано одним их catch-блоков. + +\subsection{Как ловится исключение?} + +Catch-блоки проверяются в том порядке, в котором написаны. Обработчик считается подходящим если: +\begin{enumerate} + \item + Тип, указанный в catch-блоке, совпадает с типом исключения или является ссылкой на этот тип. + \item + Класс, заданный в catch-блоке, является предком класса, заданного в throw, и наследование открытое (public). + \item + Указатель, заданный в операторе throw, может быть преобразован по стандартным правилам к указателю, заданному в catch-блоке. + \item + В catch-блоке указанно многоточие. +\end{enumerate} + +Если найдет нужный catch-блок, то выполняется его код, остальные catch-блоки игнорируются, а выполнение продолжается после try...catch-блока и исключение считается обработанным. Если ни один catch-блок не подошел, процесс раскрутки стека продолжается. + +\textcolor{red}{NB}) Так как поиск ведется последовательно, то нужно учитывать порядок catch-блоков (Например, catch(...) должен быть последним). + +\textcolor{red}{NB}) Также при наследовании классов исключений следует различать catch(type\& obj) и catch(type obj). В первом случае obj ссылается на этот объект и копии не создается. Во втором случае при входе в catch блок делается копия объекта-исключения, вследствие чего мы теряем возможность вызывать виртуальные функции. + +Пример: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +struct Exception_base { + virtual char const* msg() const { + return "base"; + } +}; + +struct Exception_derived : Exception_base { + virtual char const* msg() const { + return "derived"; + } +}; + +int f() { + try { + throw Exception_derived(); + } + catch (base e) { + std::cout << e.msg() << std::endl; + } +} + +int g() { + try { + throw Exception_derived(); + } + catch (base const& e) { + std::cout << e.msg() << std::endl; + } +} +\end{minted} + +В данном примере g() выводит <>, а функция f() выводит <>, поскольку объект исключения был скопирован с базы объекта, который мы передали в оператор throw и новая копия имеет тип base. + +В некоторых случаях внутри catch-блока может быть необходимо не завершать раскрутку стека. Для этого существует специальная форма оператора throw без аргумента. Она означает проброс текущего исключения с сохранением его типа. + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + +struct Exception_base { + virtual char const* msg() const { + return "base"; + } +}; + +struct Exception_derived : Exception_base { + virtual char const* msg() const { + return "derived"; + } +}; + +void third() { + throw Exception_derived(); +} + +void second() { + try { + third(); + } + catch (Exception_base const &obj) { + std::cout << obj.msg() << std::endl; + throw; //пробрасываем + // здесь obj имеет тип Exception_base. + // но это не мешает поймать потом тоже исключение как Exception_derived. + } +} + +void first () { + try { + second(); + } + catch (Exception_derived const &obj) { + std::cout << obj.msg() << std::endl; + } +} + +int main() { + first(); + return 0; +} + + +\end{minted} + +\textbf{Вывод программы:} \\ +> derived \\ +> derived \\ + +Значит тип объекта-параметра в текущем catch-блоке может отличаться от типа исключения, и это не влияет на дальнейшую обработку исключения в других catch-блоках. + + Например, это позволяет найти выход из такой ситуации: мы захотели вставить куда-то в глубь уже написанного кода try-catch-блок для логирования всех исключений. Тогда будем ловить по типу Exception\_all, который сделаем предком всех наших исключений. Ловим и пробрасывать дальше, чтобы не нарушать обработку производный от него исключений. + + \textcolor{red}{NB})Если необходимо изменить тип исключения, то мы можем в конце catch-блока сразу кинуть новое исключение нового типа. + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int main() +{ + try { + try { + throw derived(); + } + catch (base const& e) { + throw e; + } + } + catch (base const& e) { ... } +} +\end{minted} + +\subsection{Function-try-block} + +Часто мы хотим, чтобы все тело функции находилось в try-блоке. Тогда это try-блок называется функциональным. И для него есть отдельный синтаксис. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int main() try { + //main's body +} +catch (...) { } +\end{minted} + +Здесь функциональные try-блоки являются синтаксическим сахаром, но есть ситуации когда без них не обойтись: обработка исключений в конструкторе. + +Вот есть класс, котором мы хотим ловить и обрабатывать исключения. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class St { +public: + St(): member() { + try { + // Constructor's code + } + catch (...) { } + } +private: + Member_type member; +} +\end{minted} + +Но заметим, что вызов конструкторов членов не находится внутри try-блока и исключения возникшие в их конструкторах не поймаются. +Поэтому мы используем здесь функциональный try-блок: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class St { +public: + St() try: member() { + // Constructor's code + } + catch (...) { + + } // implicit throw +} +private: + Member_type member; +} +\end{minted} + +У функциональных try-блоков в конструкторах, есть особенность: они всегда бросают исключение повторно. + +\subsection{Уничтожение объекта при исключении в конструкторе} + +Если возникновении исключения в конструкторе некоторого объекта, объект не считается созданным и деструктор для него не вызывается. Если конструктор захватывает некоторые ресурсы и потом бросается исключение, то конструктору следует самостоятельно освободить эти ресурсы перед выбрасыванием исключения. + +При исполнении конструктора класса, вызываются конструкторы всех членов этого класса. Если исключение возникает при создании одного из членов класса, то в процессе раскрутки стека будут вызваны деструкторы от всех уже созданных членов. У самого объекта деструктор не вызывается, так как объект не считается созданным пока его конструктор не отработал полностью. + +\subsection{Best practice} +\begin{itemize} +\item +Для корректной работы с ресурсами необходимо учитывать существование исключений. То есть, если возникает исключение и некоторые из ресурсов еще не были освобождены, то следует поймать исключение и освободить оставшиеся ресурсы. + +Например, мы пишем конструктор копирования для вектора, и нам необходимо скопировать данные в другой участок памяти. При этом если во время копирование какого-то объекта возникнет исключение, то обязательно уже созданные объекты должны быть разрушены. Причем иногда важно делать это в обратном порядке их создания. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +void copy_construct(T* dist, T const * sourse, size_t size) { + size_t i = 0; + try { + for (; i != size; ++i) { + new (dist + i) T(sourse[i]); + } + } + catch (...) { // если ошибка при копировании + for (size_t j = i; j != 0; --j) { + dist[j - 1].~T(); // вызовем деструкторы созданных объектов + } + throw; + } +} +\end{minted} + +\item +Полезно знать про стандартные исключения, такие как \mintinline{c++}{std::bad_alloc}, \mintinline{c++}{std::bad_cast}, \mintinline{c++}{std::bad_typeid} и т. д. Они связанны иерархией наследования и имеют общего предка \mintinline{c++}{std::exception}. + +Подробнее можно почитать здесь: \\ +\url{https://www.tutorialspoint.com/cplusplus/cpp_exceptions_handling.htm} \\ +\url{http://en.cppreference.com/w/cpp/error/exception} \\ + +\item +Хорошим тоном является наследование от \mintinline{c++}{std::exception}. + +\item +Когда мы организовываем исключения в иерархии классов, то получаем мощный механизм описания исключение и способов их обработки. Создав такую структуры мы можем обрабатывать как более общие ошибки, так и более специализированные. + +Пример: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + class StackException {}; + class popOnEmpty(): public StackException {}; + class pushOnFull(): public StackException {}; +\end{minted} + Причем сгенерировав исключение типа \mintinline{c++}{popOnEmpty}, мы можем в разных обработчиках независимо выбирать: обработать как \mintinline{c++}{popOnEmpty} или как \mintinline{c++}{StackException}, так как тип исключения не теряется при повторной генерации этого исключения. + +\item Виртуальное наследование классов исключений: + +Рассмотрим иерархию исключений для кода, которому необходимо обрабатывать ошибки ввода. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + class InterfaceException {}; + class InputException(): public virtual InterfaceException {}; + class OutputException(): public virtual InterfaceException {}; + class InputOrOutputException: public InputException, public OutputException {}; +\end{minted} + Важно что мы наследуемся от \mintinline{c++}{InterfaceException} виртуально, так как иначе \mintinline{c++}{catch(InterfaceException const&)} не будет ловить \mintinline{c++}{InputOrOutputException}. Это связано с тем, что путь наследования без виртуального наследования будет не опреден. + +\item +Если необходимо бросать и ловить исключения из деструктора, то нужно помнить, что начиная с C++11, все деструкторы неявно помечены как \mintinline{c++}{noexcept}. Из-за этого необходимо явно указать, что дектруктор может бросать. Если этого не сделать, то при попытке выбросить исключение из деструктора вызовется функция \mintinline{c++}{std::terminate()}. + +Пример: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + class A { + ~A() noexcept(false) { + //... + } + }; +\end{minted} + +\end{itemize} +\subsection{Bad practice} +\begin{itemize} +\item +Код использующий исключения такой же быстрый как и код без них, если исключения не возникают. Если же исключения возникают, то в современных реализациях они обычно дороже проверок на коды возврата, поэтому не рекомендуется использовать исключения как часть обычного control-flow. +\end{itemize} + +\subsection{std::terminate()} + +Это функция, которая вызывается если у механизма исключений не получается корректно отработать, чтобы завершить программу. +Случаи когда она вызывается: +\begin{itemize} +\item Если исключение брошено и не поймано ни одним catch-блоком, то есть пробрасывается вне main(). +\item Если во время обработки исключения десктруктор, вызванный при раскрутке стека, бросает исключение и оно вылетает наружу. +\item Если функция переданная в \mintinline{c++}{std::atexit} и \mintinline{c++}{std::at_quick_exit} бросит исключение. +\item Если функция нарушит гарантии noexcept specification. Например, если функция помеченная как noexcept бросит исключение. +\item Если конструктор или деструктор статического или локального для одного треда объект бросает исключение. +\end{itemize} + +По умолчанию \mintinline{c++}{std::terminate()} просто вызывает \mintinline{c++}{std::abort()}, но можно это изменить, написав свою функцию \mintinline{c++}{my_terminate()} и зарегистриров ее как терминальную. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void my_terminate() { + cout << "It's not a bug, it's a feature!"; + std::abort(); +} +/**/ +set_terminate(my_teminate); +\end{minted} diff --git a/Exception_and_RAII/RAII.tex b/Exception_and_RAII/RAII.tex new file mode 100644 index 0000000..2a9fb50 --- /dev/null +++ b/Exception_and_RAII/RAII.tex @@ -0,0 +1,210 @@ +\section{RAII-classes} +\subsection{Пример} +Начнем с примера. + +У нас есть функция \mintinline{c++}{my_fopen()} для открытия файлов на чтение, которая в случаи ошибки открытия файла кидает исключение. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +FILE* my_fopen(char const* filename) { + FILE* result = fopen(filename, "r"); + if (!result) + throw std::runtime_error("fopen failed"); + + return result; +} +\end{minted} +Пусть необходимо в функции \mintinline{c++}{process_files()} открыть несколько файлов. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void process_files() { + FILE* f1 = fopen("foo"); + FILE* f2 = fopen("bar"); + FILE* f3 = fopen("baz"); + + fclose(f3); + fclose(f2); + fclose(f1); +} +\end{minted} +Но такой код не гарантирует закрытие файлов в случае возникновения исключения. +Тогда давайте вставим проверку исключений: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void process_files() { + FILE* f1 = fopen("foo"); + try { + FILE* f2 = fopen("bar"); + try { + FILE* f3 = fopen("baz"); + try { + /* ... */ + } + catch(...) { + fclose(f3); + throw; + } + fclose(f3); + } + catch(...) { + fclose(f2); + throw; + } + fclose(f2); + } + catch(...) { + fclose(f1); + throw; + } + fclose(f1); +} +\end{minted} +Итого код увеличился в несколько раз. + + +Однако, с помощью RAII-класса файловых дескрипторов \mintinline{c++}{Reader}, можно упростить код: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void process_files() { + Reader f1("foo"); + Reader f2("bar"); + Reader f3("baz"); +} +\end{minted} + +Этот код получился довольно простой и безопасный. Почему безопасный? Потому, что объект \mintinline{c++}{Reader} открывает файл в конструкторе и закрывает в деструкторе. И если возникнет исключение, то при раскрутке стека удаляться все локальные объекты, в том числе и файловые дескрипторы, закрывая файлы. Если же какой-то файл не успел открыться, то не успел и создаться объект отвечающий за него. + +Реализация \mintinline{c++}{Reader} могла бы выглядеть так: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class Reader { +public: + Reader(char const *filename): + _file(fopen(filename, "r")) { } //захват ресурса + + ~Reader() { + fclose(_file); // освобождение ресурса + } + + Reader(Reader const &) = delete; + Reader operator=(Reader const &) = delete; + +private: + FILE *_file; +}; +\end{minted} +Важно, что мы запретили копирование объектов класса, так как за один файл должен отвечать один объект. Без этого файл мог бы два раза закрыться. + +Это удобная идея получила название <>. + +\subsection{Определение} + +\textbf{<>} или <<Захват ресурса - это инициализация>> - это идиома класса, который инкапсулирует управление каким-то ресурсом. Она значит, что объект этого класса, получает доступ к ресурсу и удерживает его в течении своей жизни, а потом этот ресурс высвобождается при уничтожении объекта. +В конструкторе он должен захватить ресурс(открыть файл, выделить файл и т. д.), а в десткрукторе освободить его(закрыть файл, освободить память и т. д.). + +Также важно подумать, что должно происходить при копировании объекта, часто мы явно запрещаем это делать. Так как иначе ресурс может освободиться два раза. + +\subsection{Зачем это нужно?} +\begin{itemize} +\item Удобство кода: не нужно явно каждый раз в конце тела функции освобождать ресурсы и учитывать какие ресурсы успели захватиться, а какие нет. Когда выполнение текущего блока будет завершено, локальные объекты RAII-классов удалятся и необходимы ресурсы освободятся автоматически. +\item Безопасность исключений: Если вызывается исключение, то гарантируется, что стек очиститься и все локальные объекты удаляться, а значит и освободятся ресурсы. +\item Часто важно освобождать ресурсы в обратном порядке, относительно того, как они были захвачены. Это как раз поддерживается раскруткой стека при удалении локальных объектов. +\end{itemize} + +\textcolor{red}{NB}) Это идиома работает не только в С++, а любом языке с предсказуемым временем жизни объектов. + +\textcolor{red}{NB}) RAII часто встречается стандартной библиотеке: +\begin{itemize} +\item Smart pointers -- инкапсулируют несколько видов управления памятью +\item i/ofstream -- инкапсулиет управление файлом +\item Различные контейнеры stl -- инкапсулируют управление памятью, для хранение объектов в структурах данных. +\end{itemize} + +\subsection{Best practice} +\begin{itemize} +\item +Начиная с с++11 есть возможность хранить RAII-классы в контейнерах с помощью механизма перемещения, для этого необходимо реализовать конструктор перемещения. +\item +Вернемся к предыдущему примеру Reader. +Пусть в конструкторе есть код, который может сгенерировать исключение, тогда возникает проблема освобождения ресурса. + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +Reader::Reader(char const *filename, char const *mode) + : _file(fopen(filename, mode)) { + //код допускающий исключение +} +\end{minted} +Если в теле конструктора происходит исключение, то деструктор не вызовется, так как объект не считается созданным. Что делать? + +Решение № 1 +Написать try-catch: +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class Reader { +public: + Reader(char const *filename, char const *mode) try + : _file(fopen(filename, mode)) { + //код допускающий исключение + } + catch (...) { + destruct_obj(); + } + + ~Reader() { + destruct_obj(); + } + +private: + void destruct_obj() { + fclose(_file); + } + FILE * _file; +}; +\end{minted} +Но это вызывает сильное раздувание кода. + +Решение № 2 +Можно сделать отдельный подкласс, который хранит в себе ресурс. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class Reader { + struct ReaderHandle { + ReaderHandle(FILE *fh) + : _fh(fh) { } + + ~ReaderHandle() { + fclose(_fh); + } + + FILE *_fh; + }; +public: + Reader(char const * filename, char const * mode) + : _file(fopen(filename, mode)) { + // код допускающий исключения + } + + ~Reader() = default; + +private: + ReaderHandle _file; +}; +\end{minted} +Теперь все тоже хорошо, так как при возникновении исключения, вызовутся деструкторы от все членов класса. + +Решение № 3 (начиная с С++11) +Делегирующий конструктор. +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +class Reader +{ + Reader(FILE * file) + : _file(file) { } + +public: + Reader(char const * filename, char const * mode) + : Reader(fopen(filename, mode)) { + // код допускающий исключения + } + + ~Reader() { + fclose(_file); + } + +private: + FILE *_file; +}; +\end{minted} +Если возникнет исключение, то объект к этому времени будет считаться созданным, так как один конструктор уже отработал. Благодаря этому он уничтожится автоматически. +\end{itemize} diff --git a/Perfect_forwarding.tex b/Perfect_forwarding.tex new file mode 100644 index 0000000..8495eec --- /dev/null +++ b/Perfect_forwarding.tex @@ -0,0 +1,507 @@ +\section{Задача Perfect forwarding} + +\subsection{Формулировка проблемы, попытки тривиального решения} + + Пусть у нас есть функция g, принимающая параметр типа T. Мы хотим написать функцию f, которая примет тот же параметр типа T, сделает с ним что-нибудь (пусть в нашем примере f не делает ничего) и вызвать функцию g с этим аргументом. + + Примером пары таких f и g могут служить make\_unique(params) и конструктор T. + + make\_unique сначала вызовет конструктор T с заданными параметрами, а потом обернёт получившийся объект в unique\_ptr. В процессе передачи параметров объектов в конструктор нам и нужен perfect forwarding. + + Напишем первый вариант, который приходит на ум: + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T x) + { + g(x); + } + \end{minted} + + Этот способ имеет недостатки. Рассмотрим случай, при котором g принимает параметры по неконстантной ссылке и меняет их. Тогда + + При вызове функции f мы проинициализируем x копией параметра вызова + + В g будет передана ссылка на копию исходного параметра + + В g произойдёт изменение копии исходного параметра, исходный параметр же останется неизменным. + + Это не то поведение, которого мы ожидаем, так как g должна изменить наш параметр. + + Другое решение: заставить f принимать параметр по константной ссылке + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T const& x) + { + g(x); + } + \end{minted} + + Имеем почти ту же самую проблему: если g принимает параметр по неконстантной ссылке, вызов будет невозможен (так как неконстантная ссылка не может быть привязана к константной) + + Третье решение: заставим f принимать параметры по неконстантной ссылке + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T& x) + { + g(x); + } + \end{minted} + + Это будет проблемой при попытке вызвать f для const-объектов или для rvalue, то есть вызов f(5) и f(some\_function()) станет невозможен. + + Ни одно из приведённых выше решений не является универсальным. Возможно ли написать функцию f, работающую во всех случаях? + + Можно сделать две перегрузки f: для константных и неконстантных ссылок. + + \vspace{\baselineskip} + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T const& x) + { + g(x); + } + + template + void f(T& x) + { + g(x); + } + \end{minted} + + На первый взгляд это решение кажется хорошим, но очень скоро мы понимаем, что тогда в f нужно будет написать в два раза больше кода. К тому же, если g и f принимают по два параметра, придётся писать уже четыре перегрузки: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +void f(T& a, T& b) + +template +void f(T const& a, T& b) + +template +void f(T& a, T const& b) + +template +void f(T const& a, T const& b) +\end{minted} + +для трёх параметров - 8, а для n - $2^n$. Поэтому этот способ не применим для большого количества аргументов. + + Прежде чем рассматривать грамотное C++ 11 решение этой проблемы, давайте ближе познакомимся с правилами вывода ссылок в C++ 11. + + +\subsection{reference collapsing rule} + + Как известно, в C++ не существует ссылок на ссылки. В тех случаях, когда возникает такой тип, ссылки схлопываются (происходит reference collapsing) и получается просто одна ссылка. Например: + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template f(T & x); + + int x = 5; + f(x); + \end{minted} + + В качестве T подставляется int\&. При подстановке T -> int\& в T\&, должно была бы получится ссылка на ссылку int\& \&, но ссылки схлопываются и получается просто int\&. Следовательно сигнатура функции f после подстановки выглядит следующим образом: f(int\& x). + + Неформально правило reference collapsing в C++03 можно записать в следующем виде \& + \& = \& + + С появлением в C++11 rvalue-ссылок потребовалось доопределить правила reference collapsing для них. Эти правила могут быть неформально записаны как “одиночный амперсант всегда побеждает”. Таким образом, таблица reference collapsing в C++ 11 выглядит так: + + \& + \& = \& + + \& + \&\& = \& + + \&\& + \& = \& + + \&\& + \&\& = \&\& + + Приведём несколько примеров этого правила: + +\begin{center} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +void f(T & x); + +int y; +f(y); +\end{minted} +& +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +void f(T && x); + +f(some_function()); +//some_function возвращает int +\end{minted} +\end{tabular} + +Тогда при инстанцировании шаблона T -> int\&\&, сигнатура функции имеет вид + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& & x); +\end{minted} +& +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& && x); +\end{minted} +\end{tabular} + +После reference collapsing она превращается в: + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& x); +\end{minted} +& +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& x); +\end{minted} +\end{tabular} +\end{center} + +\subsection{Правила особого вывода ссылок} + + Пусть есть конструкция + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void func(T&& x); + \end{minted} + + Пусть есть вызов func(expression), expression имеет тип E. Тогда если expression, является lvalue, T выводится как E\&, если же expression является rvalue, то T выводится как E. + + Примеры: + + +\begin{center} +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +lvalue: & rvalue:\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{double pi = 3.14;} +& \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +func(pi);} +& +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +func(4);}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +pi - lvalue типа double, T -> double& , сигнатура func: } +& +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +4 - rvalue типа int, тогда T -> int, сигнатура func: } \\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +void func(double& &&);} +& +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{void func(int&&);} \\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +после reference collapsing & \vspace{\baselineskip} \\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\mintinline[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}{ +void func(double&); +} & \vspace{\baselineskip} \\ +\end{tabular} + +\end{center} + + + \vspace{\baselineskip} + + Отметим, что переменная типа T\&\&, использованная в функции выше, не всегда является rvalue-ссылкой, и называть её rvalue-ссылкой некорректно. В книге Скота Майерса “Эффективный и современныи С++” эти ссылки названы универсальными. Причины такого наименования станут ясны далее. + + Эти правила применяются для реешения проблемы perfect forwarding. + +\subsection{Решение проблемы perfect forwarding} + + Рассмотрим код способы передачи аргумента в g: + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T&& x) + { + //Как можно передать x в g? + g(x); //передать как lvalue + g(std::move(x)); //передать как rvalue + } + + \end{minted} + + Ни один ни другой способ не подходит. x должно передаваться так же, как получается, то есть поставлена задача сохранения value-category: пришедшее как rvalue значение, должно быть передано как rvalue, а пришедшее как lvalue - как lvalue. + + Рассмотрим вспомогательную функцию, с помощью которой мы будем это делать. + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + U&& forward(U& a) + { + return static_cast(a); + } + \end{minted} + + Тогда код, использующий её, будет выглядеть так + + \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} + template + void f(T&& a) + { + g(forward(a)); + } + \end{minted} + + Данная функция реализует perfect forwarding. Чтобы убедиться в этом рассмотрим процесс вызова f для rvalue и lvalue и посмотрим на процесс вывода и подстановки аргументов шаблона. + +\begin{center} +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +lvalue: & rvalue:\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +int a; & \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +f(a); & f(5);\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +a - lvalue типа int, поэтому T выводится в int\&, тогда после подстановки, до применения reference collapsing f выглядит так: & 5 - rvalue типа int, следовательно Т выводится в int, а после подстановки f выглядит так:\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int& && a) +{ + g(forward(a)); +} +\end{minted} +& +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& a) +{ + g(forward(a)); +} +\end{minted} +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +После применения reference collapsing, она будет выглядеть так: +& \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int& a) +{ + g(forward(a)); +} +\end{minted} +& \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +В forward в качестве U передают int\& (явно, в треугольных скобочках). Таким образом после подстановки forward выглядит так: + & В forward в качестве U передают int (явно, в треугольных скобочках), и после подстановки forward выглядит так: +\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int& && forward(int& & a) +{ + return static_cast(a); +} +\end{minted} + & \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int&& forward(int& a) +{ + return static_cast(a); +} + +\end{minted} +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +После reference collapsing: & \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int& forward(int& a) +{ + return static_cast(a); +} + +\end{minted} + & \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +Заметим, что эта функция не делает ничего, а просто кастит int\& к int\&. Поскольку forward возвращает int\&, то результат его вызова - lvalue.Значит f передаёт свой аргумент в g как lvalue. Заметим, что изначально в f a пришла как lvalue. Значит, для lvalue передача работает корректно. & Заметим, что в данном случае функция forward работает как move(приводит lvalue-ссылку к rvalue-ссылке). Результат вызова функции forward является rvalue, то есть f передаёт a как rvalue. Заметим, что получали мы её тоже как rvalue, значит, для rvalue передача работает корректно.\\ +\end{tabular} + +\end{center} + + Итого, мы сохранили value-category. forward работает либо как ничего (если ей передано lvalue), либо как move(если ей передано rvalue). + +\subsection{Одна частая ошибка при использовани forward} + Иногда при вызове forward забывают указать template-параметр в треугольных скобках. Выглядит это так: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +void f(T&& a) +{ + g(forward(a)); +} + + +\end{minted} + + Покажем, почему это ошибка и как от неё избавиться. Расмотрим вызовы для lvalue и rvalue. + +\begin{center} +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +lvalue: & rvalue:\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +int a; & \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +f(a); & f(5);\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +a - lvalue типа int, поэтому T выводится в int\&, тогда после подстановки, до применения reference collapsing f выглядит так: & 5 - rvalue типа int, следовательно Т выводится в int, а после подстановки f выглядит так:\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int& && a) +{ + g(forward(a)); +} +\end{minted} +& +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int&& a) +{ + g(forward(a)); +} + +\end{minted} +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +После применения reference collapsing, она будет выглядеть так: +& \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +void f(int& a) +{ + g(forward(a)); +} +\end{minted} +& \vspace{\baselineskip}\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +Типы для forward выводятся следующим образом: +a имеет тип int\&, а forward принимает U\&, таким образом, U выводится как int. Тогда forward выглядит следующим образом:: + & U выводится как int\&\&, тогда вместо U\& подставляется int\&\& \& (сжимается в int\&\&), а вместо U\&\& подставляется int\&\& \&\& (сжимается в int\&\&) Тогда forward выглядит следующим образом: +\\ +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int&& forward(int& a) +{ + return static_cast(a); +} + +\end{minted} + & \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +int&& forward(int& a) +{ + return static_cast(a); +} + + +\end{minted} +\end{tabular} + +\begin{tabular}{p{0.4\linewidth}p{0.4\linewidth}} +То есть forward для lvalue работает как move, а должен рабоать как ничего. & Таким образом, для rvalue forward тоже сработает как move.\\ +\end{tabular} + +\end{center} + + Мы выяснили, что, если в forward не указать параметр в треугольных скобках, то он всегда будет работать как move. + + Как можно решить эту проблему? Нужно запретить выводить тип для forward, а позволить только явно указывать его. Давайте запретим вывод типа в forward. Рассмотрим вспомогательный класс. + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +struct no_deduce +{ + typedef T type; +} +\end{minted} + + Запишем новую версию forward: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +U&& forward(typename no_deduce::type& a) +{ + return static_cast(a); +} +\end{minted} + + Заметим, что typename no\_deduce::type\& эквивалентно U\&. + + Эта конструкция запрещает вывод типов, так как при передаче значения в функцию нам нужно вывести не U\&, а typename no\_deduce::type\&, а это невыводимый контекст. Компилятор может определить, каким типом должен быть no\_deduce::type (пусть он должен иметь тип E), но он не способен вывести тип U, чтобы no\_deduce::type имел тип E (В данном конкретном случае это сделать можно (U должен совпадать с E), но в общем случае такая задача неразрешима, поэтому компилятор C++ не выводит типы в таких случаях). + + Проблема решена. + + Отметим напоследок, что forward есть в стандартной библиотеке. + +\subsection{Использование perfect forwarding} + + Perfect forwarding используется, например, при использовании функций высшего порядка - то есть функций, принимающих другие функции в качестве аргументов или возвращающих их в качестве возвращаемого значения. + + Без perfect forwarding, применение функций высшего порядка довольно обременительно, так как нет удобного способа передать аргументы в функцию внутри функции-обертки. + Возвращаясь к примеру, изложенному в начале главы, функцию-обёртку над конструктором типа T make\_unique с использованием perfect forwarding можно реализовать следующим образом: + +\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} +template +//тип T - тип, обёртываемый в shared_ptr, Args - типы аргументов конструктора. +shared_ptr make_unique(Args&&... args) +{ + return unique_ptr(new T(std::forward(args)...)) +} +\end{minted} + + Про используемые в коде variadic template можно подробнее узнать в следующей главе. diff --git a/how-to-contribute.tex b/how-to-contribute.tex index b3ee36d..e2290bf 100644 --- a/how-to-contribute.tex +++ b/how-to-contribute.tex @@ -58,37 +58,41 @@ \subsection{Linux} \textcolor{red}{NB}) \href{http://tug.ctan.org/macros/latex/contrib/minted/minted.pdf}{Документация для minted} -\subsection{Win} - TODO +\subsection{Windows} +\begin{enumerate} +\item Скачать Tex Studio +\item TODO +\end{enumerate} +\subsection{Mac OS} \section{Работа с Git} Для того, чтобы работать над конспектом необходимо использовать git. Поэтому я привел несколько ситуации, в которые вы неизбежно попадете. -{\bf Суть:} \url{https://github.com/sorokin/cpp-notes.git} - это основной репрезиторий, на котором лежит релиз конспекта. Чтобы редактировать или создавать статьи, нужно копировать его к себе на git ({\bf forked}), а потом залить в основной репрезиторий ({\bf pull request}). - \subsection{Начало работы с основным репрезиторем} +{\bf Суть:} \url{https://github.com/sorokin/cpp-notes.git} - это основной репозиторий, на котором лежит релиз конспекта. Чтобы редактировать или создавать статьи, нужно копировать его к себе на git ({\bf forked}), а потом залить в основной репозиторий ({\bf pull request}). + \subsection{Начало работы с основным репозиторем} \begin{enumerate} - \item сделать fork: на \href{https://github.com/sorokin/cpp-notes}{странице} основного репрезитория вверху справа будет нужная кнопка. (рядом с "Watch"\ ) + \item сделать fork: на \href{https://github.com/sorokin/cpp-notes}{странице} основного репозитория вверху справа будет нужная кнопка. (рядом с "Watch"\ ) - Теперь ответвление репрезитория будет у вас на гите. Вы просто работает с ним как со своим. + Теперь ответвление репозитория будет у вас на гите. Вы просто работает с ним как со своим. \item делаем clone на свой компьютер: \begin{minted}{bash} git clone git://github.com/my_name/cpp-notes.git \end{minted} \end{enumerate} \subsection{Работа с ответвлением} - Тут все просто: меняем, делаем коммит, потом push. Ведь это ваш репрезиторий. + Тут все просто: меняем, делаем коммит, потом push. Ведь это ваш репозиторий. \textcolor{red}{NB}) Для тех, кому не все просто, отправляю учить \href{https://git-scm.com/book/ru/v1}{основы git} - \subsection{Залив в основной репрезиторий} - Сначала нужно залить все изменения на свой git. Потом нужно кнопку \textbf{New pull request} и после этого ваши изменения предложатся создателю репрезетория. + \subsection{Залив в основной репозиторий} + Сначала нужно залить все изменения на свой git. Потом нужно кнопку \textbf{New pull request} и после этого ваши изменения отправятся создателю репрезетория. - \subsection{Добавление изменений основного репрезитория в ваш} - Рассмотрим такую ситуацию: вы работаете над статьей. И в это время, кто-то меняет основной репрезиторий. Как вам получить эти изменения и не потерять то, что вы уже сделали? + \subsection{Добавление изменений основного репозитория в ваш} + Рассмотрим такую ситуацию: вы работаете над статьей. И в это время, кто-то меняет основной репозиторий. Как вам получить эти изменения и не потерять то, что вы уже сделали? Решение: \begin{minted}[breaklines]{bash} - git remote add sorokin "https://github.com/sorokin/cpp-notes.git" # добавим основной репрезиторий в remote + git remote add sorokin "https://github.com/sorokin/cpp-notes.git" # добавим основной репозиторий в remote git remote -v # убедимся, что он добавился в remote git fetch sorokin # сделаем из него ветку git branch -v # убедимся, что появилась новая ветка со всем нужными нами изменениями @@ -97,13 +101,12 @@ \section{Работа с Git} \end{minted} -\textcolor{red}{NB}) После того, как мы получили ветку основного репрезитория, мы можем делать с ней все, что хотим. В принципе, можно делать rebase. +\textcolor{red}{NB}) После того, как мы получили ветку основного репозитория, мы можем делать с ней все, что хотим. В принципе, можно делать rebase. \textcolor{red}{NB}) Если кто не силен в ветвлении, смотрите все туже \href{https://git-scm.com/book/ru/v1}{книгу}. \section{LaTeX} - TODO - + Обычно все довольно не плохо гуглиться) \subsection{Структура} \begin{enumerate} diff --git a/main.tex b/main.tex index 8e24d4e..61de90c 100644 --- a/main.tex +++ b/main.tex @@ -44,5 +44,8 @@ \include{preprocessor} \include{nullptr} \include{rvalue-references} - +\include{Exception_and_RAII/Exception} +\include{Exception_and_RAII/Exception-safety} +\include{Exception_and_RAII/RAII} +\include{Perfect_forwarding} \end{document} diff --git a/nullptr.tex b/nullptr.tex index 2f3ad95..34d87db 100644 --- a/nullptr.tex +++ b/nullptr.tex @@ -1,5 +1,5 @@ \section{Нулевой указатель} -В C++ литерал 0 используется и как число 0 и как нулевой указатель. +В C++ литерал 0 используется и как число 0 и как нулевой указатель. \begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++} int i = 0; void* p = 0; // OK