std :: move
一旦开始更频繁地使用移动语义,您将开始查找要调用移动语义的情况,但您必须使用的对象是l值,而不是r值。以下面的交换函数为例:
#include <iostream>
#include <string>
template<class T>
void swap(T& a, T& b)
{
T tmp { a }; //调用复制构造函数
a = b; //调用复制分配
b = tmp; // 调用复制分配
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
swap(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}
通过两个类型为T的对象(在本例中为std :: string),此函数通过制作三个副本来交换它们的值。因此,该程序打印:
x:abc
y:de
x:de
y:abc
正如我们上一课所述,制作副本可能效率低下。这个版本的交换,生成3个副本。这会导致很多过多的字符串创建和破坏,这很慢。
但是,这里不需要复制。我们真正要做的就是交换a和b的值,这可以通过3次移动来完成!因此,如果我们从复制语义切换到移动语义,我们可以使代码更高效。
但是怎么样?这里的问题是参数a和b是l值引用,而不是r值引用,所以我们没有办法调用移动构造函数并移动赋值运算符而不是复制构造函数和复制赋值。默认情况下,我们获取复制构造函数和复制赋值行为。我们接下来做什么呢?
std ::移动
在C ++ 11中,std :: move是一个标准库函数,用于单一目的 - 将其参数转换为r值。我们可以将l值传递给std :: move,它将返回一个r值引用。std :: move在实用程序标头中定义。
这是与上面相同的程序,但是使用了一个swap()函数,它使用std :: move将我们的l值转换为r值,这样我们就可以调用移动语义:
#include <iostream>
#include <string>
#include <utility>
template<class T>
void swap(T& a, T& b)
{
T tmp { std::move(a) }; //调用move构造函数
a = std::move(b); //调用移动分配
b = std::move(tmp); // 调用移动分配
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
swap(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}
这打印出与上面相同的结果:
x:abc
y:de
x:de
y:abc
但它的效率要高得多。当初始化tmp时,我们使用std :: move将l值变量x转换为r值,而不是复制x。由于参数是r值,因此调用移动语义,并将x移动到tmp中。
通过几次交换,变量x的值已移至y,y的值已移至x。
另一个例子
当使用l值填充容器的元素(例如std :: vector)时,我们也可以使用std :: move。
在下面的程序中,我们首先使用复制语义向vector添加元素。然后我们使用移动语义向vector添加元素。
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main()
{
std::vector<std::string> v;
std::string str = "Knock";
std::cout << "Copying str\n";
v.push_back(str); // 调用push_back的l-value版本,它将str复制到数组元素中
std::cout << "str: " << str << '\n';
std::cout << "vector: " << v[0] << '\n';
std::cout << "\nMoving str\n";
v.push_back(std::move(str)); // 调用push_back的r值版本,它将str移动到数组元素中
std::cout << "str: " << str << '\n';
std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';
return 0;
}
该程序打印:
Copying str
str: Knock
vector: Knock
Moving str
str:
vector: Knock Knock
在第一种情况下,我们将push_back()传递给l值,因此它使用复制语义向vector添加元素。因此,str中的值不变。
在第二种情况下,我们将push_back()传递给r值(实际上是通过std :: move转换的l值),因此它使用移动语义向vector添加元素。这样更有效,因为vector元素可以窃取字符串的值而不必复制它。在这种情况下,str保持为空。
此时,值得重申的是std :: move()向编译器提示程序员不再需要此对象(至少不在其当前状态)。因此,您不应该对任何您不想修改的持有对象使用std :: move(),并且您不应该期望应用了std :: move()的任何对象的状态在它们之后是相同的!
移动函数应始终使对象处于明确定义的状态
正如我们在上一课中所指出的那样,总是将对象从一些明确定义的(确定性)状态中删除是一个好主意。理想情况下,这应该是一个“空状态”,其中对象被设置回其未初始化或零状态。现在我们可以谈谈为什么使用std :: move,被窃取的对象可能不是暂时的。用户可能希望再次重用此(现在为空)对象,或以某种方式对其进行测试,并可相应地进行计划。
在上面的示例中,字符串str在移动后设置为空字符串(这是std :: string在成功移动后始终执行的操作)。如果我们愿意,这允许我们重用变量str(或者我们可以忽略它,如果我们不再使用它)。
还有什么地方std :: move有用吗?
std :: move在排序元素数组时也很有用。许多排序算法(例如选择排序和冒泡排序)通过交换元素对来工作。在之前的课程中,我们不得不求助于复制语义来进行交换。现在我们可以使用移动语义,这样更有效。
如果我们想将一个智能指针管理的内容移动到另一个智能指针,它也会很有用。
Conclusion
每当我们想要将l值视为r值时,可以使用std :: move,以调用移动语义而不是复制语义。