最近在回顾cpp新特性,对左右值进行一个总结。
理论
C++中的左值引用(lvalue reference)和右值引用(rvalue reference)是两个非常重要的概念,它们对于理解C++的类型系统和编程模式有着至关重要的作用。
左值引用(lvalue reference):
- 左值(lvalue)是表示可以取地址的表达式,可以出现在赋值语句的左边。
-
左值引用使用
T&
的形式声明,比如int& lref = x; 变量x可以取地址
右值引用(rvalue reference):
-
右值(rvalue)是不能出现在赋值语句左边的表达式。它代表一个临时的、无法直接访问的对象。
-
右值引用是一个指向右值的引用。它允许你以更高效的方式操作这些临时对象。
-
右值引用使用
T&&
的形式声明,比如int&& rref = 20;
左值引用和右值引用的主要区别在于:
-
左值引用绑定到一个持久的对象,可以对其进行修改。
-
右值引用绑定到一个临时的对象,可以用来优化对这些临时对象的操作。
右值引用的引入带来了许多好处,比如:
-
移动语义(move semantics)可以减少不必要的拷贝,提高性能。
-
完美转发(perfect forwarding)可以保留函数参数的引用类型。
-
可以实现更高效的容器管理和内存分配。
进阶理解
C++左值、右值、prvalue、xvalue和glvalue等概念
-
在旧的C++标准(C++11之前)中,右值指的是临时对象或字面量(整形42,字符‘c’等),而左值指的是可以被取地址的表达式。
-
在新的C++标准(C++11及之后)中,右值被细分为两种:
- prvalue(纯右值):字面量、需要计算的表达式结果等,不能被取地址。
- xvalue(消亡值):通过将一个对象绑定到右值引用上而产生的表达式,可以被取地址。
-
123这种就是纯右值 int arr[] = {1, 2, 3, 4}; int&& x = arr[2]; //当数组下标运算符的操作数是一个数组右值时,返回的表达式就是一个消亡值。 std::string s1 = "hello"; std::string s2 = std::move(s1); //返回的是一个将 s1 绑定到右值引用的表达式,是一个消亡值(xvalue)。这样可以触发移动语义,避免不必要的拷贝。 int x = 10, y = 20; int&& z = (x > y) ? x : y; // 返回的表达式是一个消亡值,可以绑定到右值引用。 总结一下: 语义结合后(抽象理解)是右值,并且会返回出来。 比如:arr[2] 结合后返回是右值 std::move 结合后返回是右值 (x > y) ? x : y结合返回是右值
-
泛左值(glvalue)是一个更广泛的概念,包括传统意义上的左值和xvalue。所有可被取地址的表达式都是glvalue。
-
左值表达式的特点: 有合法的地址
- 变量名、函数名、一些复合类型(如数组、指针等)下标和成员访问表达式
- 前置自增/自减运算符、解引用运算符
- 可以作为赋值运算符的左操作数
- 可以被用于初始化左值引用
-
右值表达式的特点: 没合法的地址
- 不能被取地址,因为可能没有。
- 不能作为赋值运算符的左操作数,因为右值不能直接访问。
- 可以用于初始化右值引用,延长临时对象的生命周期
-
等号左边的表达式不一定是左值,因为C++允许重载赋值运算符,此时等号左边可能是一个自定义的右值类型。
回归实践
基础:
int x = 2;
int是变量类型 x是变量 即x是一个int类型的变量
int & b = x;
int是变量类型 b是变量 &是什么? &是左值引用声明符号。 即b是一个整型左值引用变量
int && a = 2;
int是变量类型 a是变量 &&是什么? &&是右值引用声明符号。 即a是一个整型右值引用变量
进阶:
int x = 10; // x is an lvalue
int y = x + 5; // x + 5 is an rvalue
int&& r1 = 42; // 42 is an rvalue, r1 is an rvalue reference
int& r2 = x; // x is an lvalue, r2 is an lvalue reference
std::string s1 = "hello";
std::string s2 = s1; // s1 is an lvalue, copy constructor is called
std::string s3 = std::move(s1); // s1 is an xvalue, move constructor is called
int arr[] = {1, 2, 3, 4};
int&& x = arr[2]; // arr[2] is an xvalue
struct Foo { int value; };
Foo foo;
Foo&& bar = std::move(foo).value; // foo.value is an xvalue
void func() {}
int&& z = (10 > 20) ? 10 : 20; // (10 > 20) ? 10 : 20 is an xvalue
int&& w = func(); // func() is a prvalue, but cannot be used to initialize a reference
技能:移动语义
前行提要
搬移构造:把石头从a搬移到b,这个过程就是搬移构造,最终石头只有一份。拥有者变了 a不再拥有。
拷贝构造:a把石头拷贝一份b,这个过程就是拷贝构造,最终石头有2份。ab都有实现共同富裕。
经典深拷贝和浅拷贝问题,因为类内有指针管理内存,使用编译器默认构造,会造成指针移动但是内存仅有一份(拷贝构造成了搬移构造),所以类内有指针一定要重写拷贝和搬移构造!!!
一句话总结就是移动语义std::move包过MyString(MyString&& other) 搬移构造
#include <iostream>
#include <string>
class MyString {
public:
MyString() : m_data(nullptr) {}
MyString(const char* str) : m_data(new char[strlen(str) + 1]) {
strcpy(m_data, str);
}
//拷贝构造
MyString(const MyString& other) : m_data(new char[strlen(other.m_data) + 1]) {
strcpy(m_data, other.m_data);
}
//搬移构造
MyString(MyString&& other) noexcept : m_data(other.m_data) {
other.m_data = nullptr;
}
~MyString() {
delete[] m_data;
}
//拷贝构造 运算符版本
MyString& operator=(const MyString& other) {
if (this != &other) {
char* temp = new char[strlen(other.m_data) + 1];
strcpy(temp, other.m_data);
delete[] m_data;
m_data = temp;
}
return *this;
}
//搬移构造 运算符版本
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
return *this;
}
private:
char* m_data; //指针!
};
int main() {
MyString s1 = "hello";
MyString s2 = std::move(s1); // move constructor is called
MyString s3 = "world";
s1 = std::move(s3); // move assignment operator is called
return 0;
}
技能:完美转发
前行提要
引用折叠技术:
引用折叠是 C++11 中模板和右值引用相关的一个重要特性。它描述了在模板实例化或类型别名定义过程中,不同引用类型如何合并或"折叠"成为最终的引用类型。
-
当两个引用类型结合时,结果是一个引用类型:
T&是函数模板 &是传参的引用符号
T& &
折叠为T&
T& &&
折叠为T&
T&& &
折叠为T&
T&& &&
折叠为T&&
-
当一个引用类型与一个非引用类型结合时,结果是一个引用类型:
T &
不会折叠T &&
不会折叠
总结一下:当函数需要左右值区分时 使用T&&写法,不需要左右值区分时 使用T或T& 写法。
所以我们该如何写引用折叠呢??
template <typename T>
void print(T& arg) {
std::cout << arg << std::endl;
}
template <typename T>
void wrapper(T&& arg) {
std::cout << arg << std::endl;
}
规范化
template <typename T>
using lref = T&;
template <typename T>
using rref = T&&;
template <typename T>
void wrapper(lref<T> arg) {
// handle lvalue
}
template <typename T>
void wrapper(rref<T> arg) {
// handle rvalue
}
新写法 decltype推导返回值类型
template <typename T>
auto wrapper(T&& arg) -> decltype(auto) {
return internal_function(std::forward<T>(arg));
}
完美转发
完美转发是一种利用右值引用和模板特性实现的技术,目的是将函数参数无损地转发给内部其他函数调用。它的核心思想是:
- 利用模板参数推导,自动推导出最合适的引用类型。
- 通过
std::forward
函数将参数完美地转发给内部其他函数。
示例
template <typename T>
void wrapper(T&& arg) {
internal_function(std::forward<T>(arg));
}
template <typename T>
void internal_function(T& lvalue) {
// handle lvalue
}
template <typename T>
void internal_function(T&& rvalue) {
// handle rvalue
}
代码举例
#include <iostream>
#include <utility>
#include <type_traits>
//T&& 区分左右值
template <typename T>
void print_reference_type(T&& t) {
if (std::is_lvalue_reference_v<decltype(t)>) {
std::cout << "t is an lvalue reference" << std::endl;
} else {
std::cout << "t is an rvalue reference" << std::endl;
}
}
int main() {
int x = 10;
int& lref = x;
int&& rref = 20;
print_reference_type(10); //r
print_reference_type(x); //l
print_reference_type(lref); //l
print_reference_type(rref); //l 当一个右值引用被用作左值时,它会退化为左值。
//forward完美转发,模板声明符号是什么传递的左右值符号就是什么!!
print_reference_type(std::forward<int&&>(x)); //r
print_reference_type(std::forward<int&&>(lref)); //r
print_reference_type(std::forward<int&&>(rref)); //r
std::cout << "Type of x: " << typeid(decltype(x)).name() << std::endl;
std::cout << "Type of lref: " << typeid(decltype(lref)).name() << std::endl;
std::cout << "Type of rref: " << typeid(decltype(rref)).name() << std::endl;
return 0;
}
输出
t is an rvalue reference
t is an lvalue reference
t is an lvalue reference
t is an lvalue reference
t is an rvalue reference
t is an rvalue reference
t is an rvalue reference
Type of x: i
Type of lref: i
Type of rref: i
总结
总的来说,C++11 及以后版本为左值和右值引入了更加细致的概念和相关特性,如左值引用、右值引用、移动语义、完美转发等。这些特性为我们提供了更加灵活和高效的编程方式,使得 C++ 在泛型编程和性能优化方面有了长足的进步。