Skip to content
This repository has been archived by the owner on Nov 12, 2021. It is now read-only.

Commit

Permalink
update chapter 2.
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoweiChen committed Jun 30, 2021
1 parent 5ae9928 commit cb740a4
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 5 deletions.
10 changes: 5 additions & 5 deletions C++-Move-Semantics.tex
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,17 @@
\subfile{content/1/chapter1/4.tex}
\subsubsection{1.5 总结}
\subfile{content/1/chapter1/5.tex}
\subsection{2 Core Features of Move Semantics}
\subsection{2 移动语义的核心}
\subfile{content/1/chapter2/0.tex}
\subsubsection{2.1 Rvalue References}
\subsubsection{2.1 右值引用}
\subfile{content/1/chapter2/1.tex}
\subsubsection{2.2 std::move()}
\subfile{content/1/chapter2/2.tex}
\subsubsection{2.3 Moved-From Objects}
\subsubsection{2.3 移动的对象}
\subfile{content/1/chapter2/3.tex}
\subsubsection{2.4 Overloading by Different References}
\subsubsection{2.4 通过引用进行重载}
\subfile{content/1/chapter2/4.tex}
\subsubsection{2.5 Passing by Value}
\subsubsection{2.5 按值传递}
\subfile{content/1/chapter2/5.tex}
\subsubsection{2.6 总结}
\subfile{content/1/chapter2/6.tex}
Expand Down
1 change: 1 addition & 0 deletions content/1/chapter2/0.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
了解了第一个使用移动语义的例子之后,本章会对移动语义的基本特征进行讨论。\par
88 changes: 88 additions & 0 deletions content/1/chapter2/1.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
\hspace*{\fill} \par %插入空行
\textbf{2.1 右值引用}

为了支持移动语义,C++引入了一种新的引用类型:\textit{右值引用}。我们讨论一下这是什么,以及如何使用。\par

\hspace*{\fill} \par %插入空行
\textbf{2.1.1 细节部分}

右值引用使用两个\&号声明。与普通引用一样,右值引用了一个存在的对象,该对象作为初始值传递。但根据语义,右值引用只能引用一个没有名称的临时对象,或使用\textit{std::move()}的对象:\par

\begin{lstlisting}[caption={}]
std::string returnStringByValue(); // forward declaration
...
std::string s{"hello"};
...
std::string&& r1{s}; // ERROR
std::string&& r2{std::move(s)}; // OK
std::string&& r3{returnStringByValue()}; // OK, extends lifetime of return value
\end{lstlisting}

右值引用来自这样一个事实:对象通常只能引用右值,这是类别,用于没有名称的临时对象和使用\textit{std::move()}的对象。\par

与成功初始化返回值引用一样,引用将返回值的生命周期延长到引用的生命周期结束(普通的const左值引用已经具有此行为)。\par

用于初始化引用的语法无关紧要。使用等号、大括号或圆括号都可以:\par

\begin{lstlisting}[caption={}]
std::string s{"hello"};
...
std::string&& r1 = std::move(s); // OK, rvalue reference to s
std::string&& r2{std::move(s)}; // OK, rvalue reference to s
std::string&& r3(std::move(s)); // OK, rvalue reference to s
\end{lstlisting}

所有这些引用都具有这样的语义:“只要对象的状态有效,就可以窃取/修改引用的对象。”编译器不会检查这些语义,因此可以像对该类型的任何非const对象那样修改右值引用,也可能不做修改。如果对一个对象有一个右值引用,该对象可能会收到一个不同的值(可能是也可能不是一个默认构造对象的值),或者保留原始值。\par

移动语义允许我们使用不再需要的值进行优化。如果编译器自动检测到从一个生命周期结束的对象中使用了一个值,将自动切换到移动语义:\par

\begin{itemize}
\item 传递一个临时对象的值,该对象将在语句执行后自动撤销。
\item 传递一个使用\textit{std::move()}的非const对象。
\end{itemize}

\hspace*{\fill} \par %插入空行
\textbf{2.1.2 作为参数的右值引用}

当我们将形参声明为右值引用时,它具有的行为和语义:\par

\begin{itemize}
\item 形参只能绑定到一个没有名称的临时对象,或者绑定到一个使用\textit{std::move()}的对象。
\item 根据右值引用的语义:
\begin{itemize}
\item[-] 调用者不再对值感兴趣。因此,可以修改参数所引用的对象。
\item[-] 但是,调用者可能仍然对使用对象感兴趣。因此,任何修改都应该使引用的对象保持有效状态。
\end{itemize}
\end{itemize}

例如:\par

\begin{lstlisting}[caption={}]
void foo(std::string&& rv); // takes only objects where we no longer need the value
...
std::string s{"hello"};
...
foo(s); // ERROR
foo(std::move(s)); // OK, value of s might change
foo(returnStringByValue()); // OK
\end{lstlisting}

你可以在通过\textit{std::move()}传递命名对象后使用,但通常不这样做。推荐的编程方式是不在\textit{std::move()}后面使用对象:\par

\begin{lstlisting}[caption={}]
void foo(std::string&& rv); // takes only objects where we no longer need the value
...
std::string s{"hello"};
...
foo(std::move(s)); // OK, value of s might change
std::cout << s << '\n'; // OOPS, you don’t know which value is printed
foo(std::move(s)); // OOPS, you don’t know which value is passed
s = "hello again"; // OK, but rarely done
foo(std::move(s)); // OK, value of s might change
\end{lstlisting}

对于标记为“OOPS”的两行,只要不对s的当前值有期望,调用在技术上是可以的。因此,打印是可以的。\par




94 changes: 94 additions & 0 deletions content/1/chapter2/2.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
如果有一个对象,当使用的时候,生存期没有结束,可以用\textit{std::move()}标记它,表示“在这里不再需要这个值。”std::move()不进行移动,它只在使用表达式的上下文中设置一个临时标记:\par

\begin{lstlisting}[caption={}]
void foo1(const std::string& lr); // binds to the passed object without modifying it
void foo1(std::string&& rv); // binds to the passed object and might steal/modify the value
...
std::string s{"hello"};
...
foo1(s); // calls the first foo1(), s keeps its value
foo1(std::move(s)); // calls the second foo1(), s might lose its value
\end{lstlisting}

带有\textit{std::move()}标记的对象仍然可以传递给接受普通const左值引用的函数:\par

\begin{lstlisting}[caption={}]
void foo2(const std::string& lr); // binds to the passed object without modifying it
... // no other overload of foo2()
std::string s{"hello"};
...
foo2(s); // calls foo2(), s keeps its value
foo2(std::move(s)); // also calls foo2(), s keeps its value
\end{lstlisting}

注意,用\textit{std::move()}标记的对象不能传递给非const左值引用:\par

\begin{lstlisting}[caption={}]
void foo3(std::string&); // modifies the passed argument
...
std::string s{"hello"};
...
foo3(s); // OK, calls foo3()
foo3(std::move(s)); // ERROR: no matching foo3() declared
\end{lstlisting}

注意,用\textit{std::move()}标记马上会销毁的对象是没有意义的。事实上,这甚至会对优化产生反效果。

\hspace*{\fill} \par %插入空行
\textbf{2.2.1 std::move()的头文件}

std::move()定义为C++标准库中的一个函数。因此,使用时需要包含头文件<utility>:\par

\begin{lstlisting}[caption={}]
#include <utility> // for std::move()
\end{lstlisting}

使用\textit{std::move()}的程序在编译时通常不包含这个头文件,因为几乎所有的头文件都包含了<utility>。但是,非标准头文件需要包含该头文件。因此,当使用\textit{std::move()}时,应该显式包含<utility>以使程序可移植。\par

\hspace*{\fill} \par %插入空行
\textbf{2.2.2 实现std::move()}

\textit{std::move()}只不过是对右值引用的static\_cast。可以手动调用static\_cast来达到相同的效果,如下所示:\par

\begin{lstlisting}[caption={}]
foo(static_cast<decltype(obj)&&>(obj)); // same effect as foo(std::move(obj))
\end{lstlisting}

因此,我们也可以这样写:\par

\begin{lstlisting}[caption={}]
std::string s;
...
foo(static_cast<std::string&&>(s)); // same effect as foo(std::move(s))
\end{lstlisting}

注意,static\_cast所做的不仅仅是改变对象的类型。它还允许将对象传递给右值引用(记住,通常不允许将具有名称的对象传递给右值引用)。我们将在关于价值类别的章节中详细讨论。\par





























88 changes: 88 additions & 0 deletions content/1/chapter2/3.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
\textit{std::move()}之后,移动的对象不会(部分)销毁。它们仍然是有效的对象,至少会为其调用析构函数。然而,它们也应该是有效的,因为它们具有一致的状态,所有操作都按照预期工作,但不知道的是它们的值。这就像使用类型的参数,而不知道传递了哪个值。\par

\hspace*{\fill} \par %插入空行
\textbf{2.3.1 有效但定义的状态}

C++标准库保证移动的对象处于有效但未定义的状态。\par

考虑如下代码:\par

\begin{lstlisting}[caption={}]
std::string s;
...
coll.push_back(std::move(s));
\end{lstlisting}

\textit{std::move()}传递s之后,可以获取字符数量,打印相应的值,甚至赋一个新值。如果不先检查字符数量,就不能打印第一个字符或任何其他字符:\par

\begin{lstlisting}[caption={}]
foo(std::move(s)); // keeps s in a valid but unclear state

std::cout << s << '\n'; // OK (don’t know which value is written)
std::cout << s.size() << '\n'; // OK (writes current number of characters)
std::cout << s[0] << '\n'; // ERROR (potentially undefined behavior)
std::cout << s.front() << '\n'; // ERROR (potentially undefined behavior)
s = "new value"; // OK
\end{lstlisting}

尽管不知道具体的值,但该字符串处于一致状态。例如,\textit{s.size()}将返回字符数,可以遍历所有有效的索引:\par

\begin{lstlisting}[caption={}]
foo(std::move(s)); // keeps s in a valid but unclear state

for (int i = 0; i < s.size(); ++i) {
std::cout << s[i]; // OK
}
\end{lstlisting}

对于用户定义类型,还应该确保移动的对象处于有效状态,这有时需要声明或实现移动操作。“移动后的状态”这一章将对此进行详细讨论。\par

\hspace*{\fill} \par %插入空行
\textbf{2.3.2 重用移动的对象}

您可能想知道为什么已移动的对象仍然是有效的对象,并且没有(部分)被销毁。原因是有一些有用的移动语义应用,在这些应用中再次使用已移动的对象是有意义的。\par

例如,考虑从流中逐行读取字符串并将其移动到vector对象中的代码:\par

\begin{lstlisting}[caption={}]
std::vector<std::string> allRows;
std::string row;
while (std::getline(myStream, row)) { // read next line into row
allRows.push_back(std::move(row)); // and move it to somewhere
}
\end{lstlisting}

每次将一行读入行后,使用\textit{std::move()}将\textit{row}的值移动到所有行的向量中。然后,s\textit{td::getline()}再次使用已移动的对象行来读入下一行。\par

第二个例子,考虑一个交换两个值的泛型函数:\par

\begin{lstlisting}[caption={}]
template<typename T>
void swap(T& a, T& b)
{
T tmp{std::move(a)};
a = std::move(b); // assign new value to moved-from a
b = std::move(tmp); // assign new value to moved-from b
}
\end{lstlisting}

我们将a的值移动到一个临时对象中,以便之后能够移动并赋值b的值。然后,被移动的对象b接收到tmp的值,这是a之前的值。\par

例如,在排序算法中使用了这样的代码,我们移动不同元素的值,使它们处于有序状态。给已移动的对象赋新值总是发生在那里。该算法甚至可以对这种移动对象的方式使用排序。\par

通常,已移动的对象应该是可以撤销的有效对象(析构函数不应该失败),重用以获得其他值,并支持类型的所有操作对象,而不需要知道具体值。“移动后的状态”这一章将对此进行详细讨论。\par

\hspace*{\fill} \par %插入空行
\textbf{2.3.3 将对象赋值给它们自己}

已移动的对象处于有效但未定义状态的规则,通常也适用于直接或间接自移动的对象。\par

例如,下面的语句之后,对象x通常在不知道其值的情况下是有效的:\par

\begin{lstlisting}[caption={}]
x = std::move(x); // afterwards x is valid but has an unclear value
\end{lstlisting}

同样,C++标准库保证对用户定义类型的实例对象通常也提供这种保证,但有时必须实现一些东西来修复默认生成的移动状态。\par


Loading

0 comments on commit cb740a4

Please sign in to comment.