forked from sorokin/cpp-notes
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrvalue-references.tex
More file actions
279 lines (219 loc) · 14.7 KB
/
rvalue-references.tex
File metadata and controls
279 lines (219 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
\section{Предыстория к теме rvalue-reference}
Здесь речь пойдет о некоторых узких моментах языка на период С++03, которые и послужили мотивацией к ввидению в язык rvalue-reference.
\subsection{Что можно положить в вектор?}
Какие требования к типу {\bf T}, чтобы можно было объявить \mintinline{c++}{vector<T>} и потом этим пользоваться:
\begin{enumerate}
\item CopyConstructor - можно копировать
\item CopyAssignable - можно присваивать
\item Destroyble - можно разрушать
\end{enumerate}
Получается, что этим условиям не удовлетворяют: reference, const T, stream, RAII-classes\footnote{ RAII (resource acquisition is initialization) - идиома управления ресурсами, когда объект "захватывает" ресурс, то есть только он может к нему обращаться.}
Как поступать в таких случаях? Можно так:
\begin{enumerate}
\item хранить указатель => но тогда нужно иметь дело с динамической памятью и неудобным синтаксисом, и вообще это очень плохо:
\begin{minted}[
linenos,
frame=lines,
framesep=2mm,
breaklines]
{c++}
vec.push_back(new T(...)) // если при создании будет ошибка от resize() при выделении памяти, то объект не удалится
\end{minted}
\item unique\_ptr => это не плохой вариант
\item shared\_ptr => неудобный синтаксис \textbf{vec.push\_back(shared\_ptr<file\_descriptor>(new file\_descriptor()))}
\begin{minted}[
linenos,
frame=lines,
framesep=2mm]
{c++}
vec.push_back(shared_ptr<file_descriptor>(new file_descriptor(...)))
\end{minted}
\end{enumerate}
\subsection{Shared\_ptr}
\mintinline{c++}{std::shared_ptr} - это умный указатель, с разделяемым владением объектов через его указатель. Несколько указателей могут владеть одним объектом. Объект будет уничтожен, когда последний \mintinline{c++}{shared_ptr} будет уничтожен или сброшен.
\textcolor{red}{NB}) \mintinline{c++}{shared_ptr} может не указывать ни на какой объект.
\subsubsection{Наивная реализация.}
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
template <typename T, typename D>
struct shared_ptr {
T* obj; // указатель на объект управления
size_t* counter; // счетчик ссылок, обнуление которого влечет удаление объекта
D *deleter; // функция, которая удаляет объект
shared_ptr(*T, D const& deleter);
}
\end{minted}
\textcolor{red}{NB}) \textbf{Зачем делитер, если можно всегда вызвать деструктор объекта?} Ответ: Ингда мы хотим реализовать следующую схему пользования объектом: 1) мы берем объект из ресурса. 2) пользуемся им. 3) Потом возвращаем его от куда взяли, когда он стал нам не нужен.
\subsubsection{Оптимизация по памяти.}
На самом деле все немного сложнее: нам может понядобиться хранить в нашем smart\_ptr аллокатор, счетчик weak\_ptr (об этом позднее), указалеть на объект управления. Поэтому имеет смысл хранить все это в одном объекте, для оптимизации выделения памяти под smart\_ptr. С учетом выше указанного:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
template <typename T, typename D>
struct shared_ptr {
T* obj; // указатель на объект управления
struct control_block {
size_t* counter; // счетчик ссылок, обнуление которого влечет удаление объекта
D *deleter; // функция, которая удаляет объект
}* con_bl;
shared_ptr(*T, D const& deleter);
};
\end{minted}
\textcolor{red}{NB}) если создавать shared\_ptr с помощью конструктора, то память будет распределяться как выше указанно, но если создать shared\_ptr с помошью \mintinline{c++}{std::make_shared(new T)}\footnote{создает объект и оборачивает его в shared\_ptr}, то память выделится только один раз, и объект будет лежать рядом с блоком управления. Поэтому лучше использовать последний способ где это возможно, чтобы снизить оверхед от "умности"\ указателя.
\subsubsection{Специальный конструтор.}
Иногда мы хотим продлевать объекту жизнь, если что-то ссылается на его поля. Для этого существует специальный конструтор:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct car {
Wheel wheel[4];
}
// У нас есть машина с колесами. Мы хотим, чтобы пока мы умеет живой указатель на колесо, машина тоже не удалялась вместе с колесами.
/* ... */
shared_ptr<car> p;
shared_ptr<wheel> q(&p, wheel[2]); // мы переиспользуем p.counter для q.
\end{minted}
Для этого наверно, будет полезно иметь ссылку на объект в блоке управления.\footnote{Мнение автора.}
\textcolor{red}{NB}) Еще одно применение этой фичи: мы хотим хранить дерево по shared\_ptr, чтобы когда мы удалили корень, дерево само удалиться. Тогда с помощью этого, конструктора можно гарантировать, что если есть указатель на вершины дерева, то можно гарантировать, что по нему можно пробежаться.
\subsubsection{Заметки по использованию.}
\begin{enumerate}
\item
Возможно глупый пример, но все же.
\begin{minted}[
linenos,
frame=lines,
framesep=2mm,
tabsize = 4,
breaklines]
{c++}
int *silly_ptr = new int(5);
shared_ptr<int> a1(silly_ptr);
shared_ptr<int> b1(silly_ptr);
// b1.counter = a1.counter = 1;
// разные управляющие блоки
shared_ptr<int> a2(silly_ptr);
shared_ptr<int> b2(a2);
// b2.counter = a2.counter = 2;
// одинаковые
\end{minted}
\end{enumerate}
\subsubsection{Weak\_ptr}
\mintinline{c++}{std::weak_ptr} - умный указатель, который моделирует временное владение объектом.
Ситуация: мы хотим хешировать картинки, которые хранять по shared\_ptr. То есть нам нужен мэп: name\_image $\to$ shared\_ptr. Но есть проблема, что если мы будем хранить в мэпе shared\_ptr, то они будут считать владельцами этих картинок, и они никогда не будут удаляться (если только их не удалить руками). В этой ситуации может помочь \mintinline{c++}{std::weak_ptr}.
Дело в том, что weak\_ptr содержит "слабую"\ ссылку на объект, управляемый указателем shared\_ptr. Для этого и нужен счетчик "слабых" сслылок в блоке управления shared\_ptr. Теперь блок управления считает еще и слабые сслыки и когда удаляется управляемый объект, блок управления остается жить, пока на него ссылается хотя бы один weak\_ptr, чтобы сообщать им о существовании объекта. Например с помощью метода \mintinline[breaklines]{c++}{std::weak_ptr::expired() // проверяет удален ли объект.}
То есть все, что должен хранить внутри себя weak\_ptr - это указатель блок управления.
Теперь вернемся к нашей задаче: мы сделаем мэп: name\_image $\to$ weak\_ptr.
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
map<string, weak_ptr<image>> cache;
shared_ptr<image> load_image(string const& name) {
auto i = cache.find(name);
if (i != cache.end()) {
shared_ptr<image> temp = i->second.lock(); // создаем shared_ptr, который управляет объектом, которым управляет i. Если объект удален temp будет пустой.
if (temp) {// проверка на то, что temp не пуст
return temp;
}
}
// если картинки нет в кэше загружаем ее.
shared_ptr<image> temp = real_load(name);
cache[name] = temp; // переобразование weak_ptr(shared_ptr);
return temp;
}
\end{minted}
\textcolor{red}{NB}) Преобразования между shared\_ptr и weak\_ptr - это нормально. Только с его помощью можно обратиться к объекту weak\_ptr, не определяя новых указателей.
\textcolor{red}{NB}) С помощью weak\_ptr можно решать циклические зависимости shared\_ptr. Суть проблемы в том, что объекты не могут удалиться, так как ссылаются друг на друга.
\subsection{Сast\_pointer}
Что выполнять безоспасное приведение умных указателей можно искользовать слудующий синтаксис:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
struct A {/*...*/};
struct B: A {/*...*/};
shared_ptr<A> foo;
shared_ptr<B> bar;
/*...*/
bar = make_shared<B>();
foo = dynamic_pointer_cast<A>(bar);
// иначе пришлось бы писать так:
foo = shared_ptr<A>(dynamic_cast<A>(bar.get()));
\end{minted}
\subsection{Когда необходимо копирование?}
В С++ мы можем принимать аргументы функции по значению или по ссылке. И возвращать также. Меняется то, передаем ли мы сам объект или только его копию(значение). То есть если мы не хотим, чтобы объект менялся, то мы передаем его по значению, иначе по ссылке.
Ситуация:
\begin{minted}[linenos, frame=lines, framesep=2mm, tabsize = 4, breaklines]{c++}
void f(T); // получение по значению
T g(); // возврат по значению
int main() {
f(g()); // здесь могут быть лишние копирования.
}
\end{minted}
Возникает вопрос: \textbf{Зачем нам копировать временные объекты, к которым все равно никто не может обратиться?}
Поэтому иногда компилятор творит магию. Обозначение в таблице:
\begin{enumerate}
\item В таблице приведен код на C++, а рядом его эквивалент в языке C.
\item void* - это указатель на неинициализированную память, инача T*
\end{enumerate}
\begin{table}[H]
\begin{tabular}{|p{0.5\textwidth}|p{0.5\textwidth}|}
\hline
\multicolumn{2}{|p{0.5\textwidth}|}{Простой пример} \\
\hline
\begin{minted}[linenos, tabsize = 4, breaklines]{c++}
// C++
T obj(1, 2, 3);
\end{minted}
&
\begin{minted}[linenos, tabsize = 4, breaklines]{c}
// C
char obj_storage[sizeof(T)];
char_T(&obj_storage, 1, 2, 3);
\end{minted}
\\
\hline
\multicolumn{2}{|p{0.5\textwidth}|}{copy\_edision} \\
\hline
\begin{minted}[linenos, tabsize = 4, breaklines]{c++}
// C++
T obj = g(); // => T obj(g());
\end{minted}
&
\begin{minted}[linenos, tabsize = 4, breaklines]{c}
// C
char obj_storage[sizeof(T)];
char_T(&obj_storage, obj);
\end{minted}
\\
\hline
\multicolumn{2}{|p{0.5\textwidth}|}{return\_value\_optimization (RVO)} \\
\hline
\begin{minted}[linenos, tabsize = 4, breaklines]{c++}
// C++
T g() {
return T(1, 2, 3);
}
\end{minted}
&
\begin{minted}[linenos, tabsize = 4, breaklines]{c}
// C
void g(void *result) {
char_T(result, 1, 2, 3);
}
\end{minted}
\\
\hline
\multicolumn{2}{|p{0.5\textwidth}|}{name\_return\_value\_optimization (NRVO)} \\
\hline
\begin{minted}[linenos, tabsize = 4, breaklines]{c++}
// C++
void g() {
T temp(1, 2, 3);
temp.push_back(/*...*/);
return temp;
}
\end{minted}
&
\begin{minted}[linenos, tabsize = 4, breaklines]{c}
// C
void g(void *result) {
char_T(result, 1, 2, 3);
((*T)result)->push_back(/*...*/);
}
\end{minted}
\\
\hline
\end{tabular}
\end{table}
\textcolor{red}{NB}) Из таких оптимизаций, мы избегаем лишних копирований. Но могут быть и отрицательные эффекты: например нам теперь запрещено даже думать об глобальных побочных эффектах от конструкторов копирования.
\textcolor{red}{NB}) Может быть проблема с исключениями: ??? %TODO