一、左值和右值
在前面谈过左值和右值,这里不再谈。需要注意的是左值有引用,那么新的c++标准中,右值也是有引用的。在c++新标准中,细分了lvalue,rvalue,glvalue(泛左值),prvalue(纯右值),xvalue(消亡值)。这些详细的说明可以参看一下c++11以上的标准文档。一般人都是以等号左右来区分左右值,其实这是不完全准确的。其实无法取地址的即为右值。不要迷惑的是,右值引用也是左值。
二、移动语义
移动语义和右值是密不可分的,这也是c++不断的引入右值来处理一些问题的原因。大家都知道在std::vector向量的插入操作过程中,需要不断的进行临时变量的复制和销毁,这对于c++编程人员来说,是不可忍受的(当然,指的是有些人)。能不能够做到这些临时对象的变量不再复制而是直接使用。这样既减少了内存复制的开销,又减小了释放对象的过程,一举多得,再好不过。
解决问题的方法有两类,一种是传统的使用输出参数,也就是说不使用返回值而是直接通过参数来传递结果;另外一种就是移动语义。
所谓移动语义,其实就是上面提到的把对象转移到指定的目标,而不是通过复制。它分为显示和隐式两种。显示的移动一个对象可以使用std::move这个函数来实现;而隐式的可以使用移动构造函数、移动赋值操作符等来实现。移动语义的出现,可以说对c++的操作运算有一个补全和提高效率的作用,而这也恰恰是c++的优势,也就是说,在优势上,更加具有了优势。
std::unique_ptr是c++11中的一个只能移动不能复制的类型,如果写过条件变量的锁操作的程序员可能印象会比较深刻。移动后的对象,原操作就不能再使用了,否则可能会引起不可知的后果(可能是崩溃)。一般在编写移动成员时,建议增加noexcept关键字,这个很重要。
在c++的传统面试题中,有一个拷贝构造函数的编写,其中使用“复制交换”可以提高效率,那么同样也可以使用“移动交换”,既可以提高效率又可以减少开销。
三、实例
说多少不如看一个例子更明白:
#include <string>
#include <iostream>
#include <utility>
struct A
{
std::string s;
A() : s("test") { }
A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }
A(A&& o) : s(std::move(o.s)) { }
A& operator=(const A& other)
{
s = other.s;
std::cout << "copy assigned\n";
return *this;
}
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "move assigned\n";
return *this;
}
};
A f(A a) { return a; }
struct B : A
{
std::string s2;
int n;
// implicit move assignment operator B& B::operator=(B&&)
// calls A's move assignment operator
// calls s2's move assignment operator
// and makes a bitwise copy of n
};
struct C : B
{
~C() { } // destructor prevents implicit move assignment
};
struct D : B
{
D() { }
~D() { } // destructor would prevent implicit move assignment
D& operator=(D&&) = default; // force a move assignment anyway
};
int main()
{
A a1, a2;
std::cout << "Trying to move-assign A from rvalue temporary\n";
a1 = f(A()); // move-assignment from rvalue temporary
std::cout << "Trying to move-assign A from xvalue\n";
a2 = std::move(a1); // move-assignment from xvalue
std::cout << "Trying to move-assign B\n";
B b1, b2;
std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
b2 = std::move(b1); // calls implicit move assignment
std::cout << "After move, b1.s = \"" << b1.s << "\"\n";
std::cout << "Trying to move-assign C\n";
C c1, c2;
c2 = std::move(c1); // calls the copy assignment operator
std::cout << "Trying to move-assign D\n";
D d1, d2;
d2 = std::move(d1);
}
这个例子还是比较全面的。看一下那个移动交换的:
template <typename T>
Array<T>&Array<T>::operator=(Array && rhs) noexcept
{
Array<T> moved(std::move(rhs));
swap(moved);
return *this;
}
- 这个例子出自《c++17入门经典》,里面有一个对比的例子,有兴趣可以下载看一看,很有说明力。
大家可以好好的和复制交换比一比。
四、总结
vistual studio2022中,对c++20的支持很好(默认是c++14,下面有c++17、c++20和更新的c++23等),所以如果对Linux命令行有抵触的一些程序员,接触新的c++标准,建议使用这个。但对广大的老鸟儿们来说,在Linux上搞更新的c++才能体现自己的狂野的一面,“工欲善其事,必先利其器”,手段不同而已,不要过多纠结。
对于新标准,要勤学多用,特别在工程实践上,用得多了,自然就熟悉了,很自然就融于自己的思想过程,正所谓信手拈来,无一而不至其极!