左值和右值
C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。
判断方法
有两种方法可以判断是左值还是右值
-
可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
-
有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
右值引用
c++11 引入右值引用
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。能够更简洁明确地定义泛型函数。
性质
和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化
int a = 10;
// int && ra = a; ERROR, 不能用左值初始化右值引用
int && ra = 20;
cout << ra << endl; // 输出 20
右值引用可以对值进行修改
int && ra = 20;
ra = 40;
cout << ra << endl; // 输出 40
std::move
右值引用通过 std::move 函数来实现所有权的转移。std::move 是一个函数模板,将传递的对象转换为右值引用,标记其状态为可移动的。这样,在移动构造函数、移动赋值运算符或其他需要右值引用的操作中,可以利用对象的移动语义来完成高效的资源转移。
当使用右值引用绑定到一个对象时,编译器会将该对象的类型推断为右值引用类型,并允许在该对象上执行移动操作。通过移动构造函数或移动赋值运算符,可以将资源的所有权从源对象转移到目标对象,通常是通过简单的指针或指针成员的转移。
移动语义的实现是通过类型的设计和实现来完成的。对于复杂类型,需要定义移动构造函数和移动赋值运算符,并在这些操作中进行资源的所有权转移。对于简单的内置类型,移动语义与拷贝语义相同, 因为内置类型的赋值和拷贝操作是通过简单的按位复制来完成的,不涉及任何资源的动态分配或释放。
总结起来,右值引用和移动语义的引入使得在 C++ 中可以更高效地实现资源的转移和管理,避免不必要的数据复制和资源分配。这对于提高性能和优化内存使用是非常有益的。
int main() {
string str("I'm a string");
string str1 = std::move(str);
cout << "str: " << str << endl;
cout << "str1: " << str1 << endl;
int a = 10;
int b = std::move(a);
cout << "a: " << a << endl;
cout << "b: " << b << endl;
return 0;
}
输出
str: // str 的值变为空
str1: I'm a string
a: 10 // 简单内置类型,移动语义还是拷贝
b: 10
在 stl 容器中使用,减少深拷贝
string str("I'm a string");
vector<string> vec;
vec.push_back(std::move(str));
cout << "str: " << str << endl; // str 为空
cout << "vec[0]: " << vec[0] << endl;
std::forward
实现完美转发,std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
使用完美转发可以减少不必要的拷贝或修改,提高性能。
引用折叠规则:
-
当一个对象被右值引用(&&)折叠时,结果仍然是右值引用。
T&& && -> T&&
-
当一个对象被左值引用(&)折叠时,结果仍然是左值引用。
T& & -> T&
-
当一个右值引用和一个左值引用折叠时,结果是左值引用。
T& && -> T&
T&& & -> T&
example:
A 有两个构造函数,一个参数是左值,一个参数是右值
B 初始化的时候会创建 3 个 A 的对象,根据传入的参数,自动调用 A 对应的构造函数。
struct A
{
A(int&& n) { std::cout << "rvalue overload, n=" << n << '\n'; }
A(int& n) { std::cout << "lvalue overload, n=" << n << '\n'; }
};
class B
{
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{}
private:
A a1_, a2_, a3_;
};
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
template<class T, class... U>
std::unique_ptr<T> make_unique2(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}
auto make_B(auto&&... args) // since C++20
{
return B(std::forward<decltype(args)>(args)...);
}
int main()
{
auto p1 = make_unique1<A>(2); // rvalue
int i = 1;
auto p2 = make_unique1<A>(i); // lvalue
std::cout << "B\n";
auto t = make_unique2<B>(2, i, 3);
std::cout << "make_B\n";
B b = make_B(4, i, 5);
}
输出:
rvalue overload, n=2
lvalue overload, n=1
B
rvalue overload, n=2
lvalue overload, n=1
rvalue overload, n=3
make_B
rvalue overload, n=4
lvalue overload, n=1
rvalue overload, n=5