这里写目录标题
- 左值
- 右值
- 左值引用和右值引用
- 右值引用和移动构造函数
- std::move 移动语义
- 返回值优化
- 移动操作要保证安全
- 万能引用
- std::forward 完美转发
- 传入左值
- 传入右值
左值
左值是指可以使用 & 符号获取到内存地址的表达式,一般出现在赋值语句的左边,比如变量、数组元素和指针等。
下面是左值的举例:
int i = 42; // i 是一个 left value
int *p = &i // i 是一个左值,可以通过 & 获取内存地址
int& ldemoFoo() {return i;
}ldemoFoo() = 42;// 这里使用函数返回引用的形式, ldemoFoo 是一个左值
int *p1 = &ldemoFoo()
左值(lvalue)代表的是对象的身份和它在内存中的位置,它一般出现在赋值语句的左侧,左值通常是可修改的(可以修改的左值)。
Notice : 左值代表一个具体的对象位置,在内存中有明确位置,通常是可以修改的
左值的特点包括:
- 可寻址性:左值可以取得地址,即你可以使用 & 运算符来取得一个左值的地址。
- 可赋值性:左值可以出现在赋值语句的左侧,你可以将一个值赋给它。
下面类型的值都是左值:
- 变量名:如 int x;,x是一个左值。
- 数组元素:如 arr[0],arr[0] 是一个数组的左值元素。
- 结构体或类的成员:如 obj.member ,obj.member 是一个对象的左值成员。
- 解引用的指针:如 *ptr,*ptr 是通过指针访问的对象的左值。
并非所有的左值都是可修改的。例如,const 限定的左值就不应该被修改。
右值
在C++中,右值(rvalue)是指哪些不代表内存的中具体位置, 不能被赋值和取地址的值 。
一般出现在赋值操作符的右边,表达式结束就不存在的临时变量。
右值的典型例子包括 字面量、临时对象以及某些表达式的结果。
右值主要用来表示数据值本身,而不是数据所占据的内存位置。
右值的关键特性包括:
- 不可寻址性:右值不能取得地址,尝试对右值使用 & 运算符会导致编译错误。
- 可移动性: 由于右值不代表持久的内存位置,因此可以安全地 “移动” 它们的资源到另一个对象,而无需进行复制。这就是为什么右值经常与移动语义一起使用。
- 临时性:许多右值是临时对象,它们在表达式结束后就会被销毁。
Notice:右值必须要有一个包含内存地址变量去接收这个指,否则就会丢弃
C++ 中右值的例子有:
- 字面量:比如整数10、字符’A’、浮点数3.14。
- 函数返回的临时值:如 getRandomNumber() 返回的随机数,注意函数也有可能返回左值。
- 由运算符产生的值:比如表达式 a + b 的结果,假设 a 和 b 是数值类型的变量。
- 空指针常量:nullptr。
- 字符串字面量:比如"hello world"。
- 类的右值构造函数或移动构造函数生成的临时对象:如 MyClass() 创建的临时对象。
- 通过std::move()转换得到的右值:std::move(myObject),其中 myObject 是一个左值。
- 数组下标的表达式:如果数组是右值,那么数组下标也是右值,例如 arr[0],其中arr是一个临时数组。
- 类成员的右值访问:如果类有一个返回右值的成员函数,那么该函数返回的结果是右值。
// 10 'A' 都是右值字面量
int a = 10;
char b = 'A';// generateResult 返回值是一个临时对象,也就是右值
int a = generateResult(20, 10);
// a + b 产生的结果也是右值
int m = a + b;
// "hello world "是右值
const char *pName = "hello world";
// nullptr 是右值
int32_t *p = nullptr;DemoClass p = DemoClass();
注意函数返回值不一定只能是右值,也有可能是左值,比如返回引用的情形
int& testlvaluefuncyion() {int i;return i;
}int testrvaluefuncyion() {int i = 5;return i;
}{testlvaluefuncyion();// 正确,函数返回值可作为左值testlvaluefuncyion() = 10;int *p1 = &testlvaluefuncyion();std::cout << "function return value as leftvalue" << std::endl;
}// 函数返回值是int类型,此时只能作为右值
{testrvaluefuncyion();//testrvaluefuncyion() = 10;std::cout << "function return value as rightvalue" << std::endl;
}
左值引用和右值引用
C++中的引用是一种别名,代表的就是变量的地址本身,可以通过一个变量别名访问一个变量的值。
int &a = b 表示可以通过引用 a 访问变量 b , 注意引用实际就是指向变量 b,等于是变量 b 的别名
左值引用是指对左值进行引用的引用类型,通常使用 & 符号定义
右值引用是指对右值进行引用的引用类型,通常使用 && 符号定义
C++11引入了右值引用,允许我们将右值绑定到引用上。这在 移动语义和 完美转发 等高级功能中非常有用。
class DemoClass {...};
// 接收一个左值引用
void foo(X& x);
// 接收一个右值引用
void foo(X&& x);X x;
foo(x); // 传入参数为左值,调用foo(X&);X bar();
foo(bar()); // 传入参数为右值,调用foo(X&&);
通过重载左值引用和右值引用两种函数版本,满足在传入左值和右值时触发不同的函数分支。 注意 void foo(const X& x);
同时接受左值和右值传参。
void foo(const X& x);
X x;
foo(x); // ok, foo(const X& x)能够接收左值传参X bar();
foo(bar()); // ok, foo(const X& x)能够接收右值传参// 新增右值引用版本
void foo(X&& x);
foo(bar()); // ok, 精准匹配调用foo(X&& x)
定义右值引用的方法:
int a = 10;
// 定义左值引用
int &lvalue_ref = a; // 定义右值引用
int &&rvalue_ref = 10 + 20;
右值引用和移动构造函数
假设定义一个类 DemoContainerClass,包含一个指针成员变量 p,该指针指向了另一个成员变量 DemoBasicClass,假设 DemoBasicClass 占用了很大的内存,创建和复制 DemoBasicClass 都需要很大的开销。
class DemoBasicClass {
public:DemoBasicClass() {std::cout << __FUNCTION__ "construct call" << std::endl;}~DemoBasicClass() = default;DemoBasicClass(const DemoBasicClass& ref) {std::cout << __FUNCTION__ "copy construct call" << std::endl;}
};class DemoContainerClass{
private:DemoBasicClass *p = nullptr;
public:DemoContainerClass() {p = new DemoBasicClass();}
~DemoContainerClass() {if( p != nullptr) {delete p;}
}
DemoContainerClass(const DemoContainerClass& ref) {std::cout << __FUNCTION__ "copy construct call" << std::endl;p = ref.p;
}DemoContainerClass& operator=(const DemoContainerClass& ref) {std::cout << __FUNCTION__ "operator construct call" << std::endl;DemoBasicClass* tmp = new DemoBasicClass(*ref.p);delete this->p;this->p = tmp;return *this;
}
上面定义了 DemoContianerClass 的赋值构造函数 ,现在假设有下面的场景
{DemoContainerClass p;DemoContainerClass q;p = q;
}
输出如下:
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =operator construct call
rValurRefDemo::DemoBasicClass::DemoBasicClasscopy construct call
DemoContainerClass p
和 DemoContainerClass q
初始化时,都会执行 new DemoBasicClass
,所以会调用两次 DemoBasicClassconstruct
,执行 p = q 时,会调用一次 DemoBasicClass
的拷贝构造函数,根据 ref 复制出一个新结果。
由于 q 在后面的场景还是可能使用的,为了避免影响 q,在赋值的时候调用DemoBasicClass
的构造函数复制出一个新的 DemobasicClass
给 p 是没有问题的。
但在下面的场景下,这样是没有必要的
static rValurRefDemo::DemoContainerClass demofunc() {return rValurRefDemo::DemoContainerClass();
}{DemoContainerClass p;p = demofunc();
}
这种场景下,demofunc 创建的那个临时对象在后续的代码中是不会用到的,所以我们不需要担心赋值函数中会不会影响到那个 DemobasicClass
临时对象,也就没有必要创建一个新的 DemoBasicClass
类型给 p,
更高效的做法是,直接使用 swap 交换对象的 p 指针,这样做有两个好处:
- 不需要调用 DemobasiClass 的构造函数,提高效率
- 交换之后,demofunc 返回的临时对象拥有 p 对象的 p 指针,在析构时可以自动回收,避免内存泄漏
这种避免高昂的复制成本,从而直接将资源从一个对象移动到另一个对象的行为,就是C++的 移动语义。
哪些场景适合移动操作呢?无法获取内存地址的右值就很合适,我们不需要担心后续的代码会用到这个值。
添加移动赋值构造函数如下:
DemoContainerClass& operator=(DemoContainerClass&& rhs) noexcept {std::cout << __FUNCTION__ "move construct call" << std::endl;std::swap(this->p, rhs.p);return *this;
};
输出结果如下:
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call
std::move 移动语义
C++提供了std::move函数,这个函数做的工作很简单:通过隐藏掉入参的名字,返回对应的右值。
std::cout << "#############################################" << std::endl;
{DemoContainerClass p;DemoContainerClass q;// OK 返回右值,调用移动赋值构造函数 q,但是 q 以后都不能正确使用了p = std::move(q);}std::cout << "#######################################" << std::endl;
{DemoContainerClass p;// OK 返回右值,调用移动赋值构造函数 效果和 demofunc 一样p = std::move(demofunc());
}
输出结果如下:
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call
一个容易犯错的例子:
class Base {
public:// 拷贝构造函数Base(const Base& rhs);// 移动构造函数Base(Base&& rhs) noexcept;
};class Derived : Base {
public:Derived(Derived&& rhs)// wrong. rhs是左值,会调用到 Base(const Base& rhs).// 需要修改为Base(std::move(rhs)): Base(rhs) noexcept {...}
}
返回值优化
考虑下面的情形:
DemobasicClass foo() {DemobasicClass x;return x;
};DemobasicClass bar() {DemobasicClass x;return std::move(x);
}
大家可能会觉得 foo 需要一次复制行为:从 x 复制到返回值;bar 由于使用了 std::move,满足移动条件,所以触发的是移动构造函数:从x移动到返回值。复制成本大于移动成本,所以 bar 性能更好。
实际效果与上面的推论相反,bar中使用std::move反倒多余了。现代C++编译器会有返回值优化。换句话说,编译器将直接在foo返回值的位置构造x对象,而不是在本地构造x然后将其复制出去。很明显,这比在本地构造后移动效率更快。
移动操作要保证安全
比较经典的场景是std::vector 扩缩容。当vector由于push_back、insert、reserve、resize 等函数导致内存重分配时,如果元素提供了一个 noexcept 的移动构造函数,vector 会调用该移动构造函数将元素移动到新的内存区域;否则 则会调用拷贝构造函数,将元素复制过去。
万能引用
完美转发是C++11引入的另一个与右值引用相关的高级功能。它允许我们在函数模板中将参数按照原始类型(左值或右值)传递给另一个函数,从而避免不必要的拷贝和临时对象的创建。
为了实现完美转发,我们需要使用 std::forward 函数和通用引用(也称为转发引用)。通用引用是一种特殊的引用类型,它可以同时绑定到左值和右值。通用引用的语法如下:
通用引用的形式如下:
template<typename T>
void foo(T&& param);
万能引用的ParamType是T&&,既不能是const T&&,也不能是std::vector&&
通用引用的规则有下面几条:
- 如果 expr 是左值, T 和 param 都会被推导为左值引用
- 如果 expr 是右值, T会被推导成对应的原始类型, param会被推导成右值引用(注意,虽然被推导成右值引用,但由于param有名字,所以本身还是个左值)。
- 在推导过程中,expr的const属性会被保留下来。
参考下面示例:
template<typename T>
void foo(T&& param);// x是一个左值
int x =2 7;
// cx 是带有const的左值
const int cx = x;
// rx 是一个左值引用
const int& rx = cx;// x是左值,所以T是int&,param类型也是int&
foo(x);// cx是左值,所以T是const int&,param类型也是const int&
foo(cx);// rx是左值,所以T是const int&,param类型也是const int&
foo(rx);// 27是右值,所以 T 是int,param类型就是 int&&
foo(27);
std::forward 完美转发
template<typename T, typename Arg>
std::shared_ptr<T> factory_v4(Arg&& arg)
{ return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}// std::forward的定义如下
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{return static_cast<S&&>(a);
}
传入左值
int p;
auto a = factory_v4§;
根据万能引用的推导规则,factory_v4中的 Arg 会被推导成 int&。这个时候factory_v4 和 std::forwrd等价于:
shared_ptr<A> factory_v4(int& arg)
{ return shared_ptr<A>(new A(std::forward<int&>(arg)));
}int& std::forward(int& a)
{return static_cast<int&>(a);
}
这时传递给 A 的参数是 int&, 调用的是拷贝构造函数 A(int& ref), 符合预期
传入右值
auto a = factory_v4(3);
shared_ptr<A> factory_v4(int&& arg)
{ return shared_ptr<A>(new A(std::forward<int&&>(arg)));
}int&& std::forward(int&& a)
{return static_cast<int&&>(a);
}
此时,std::forward作用与std::move一样,隐藏掉了arg的名字,返回对应的右值引用。
这个时候传给A的参数类型是X&&,即调用的是移动构造函数A(X&&),符合预期。
####重要参考
深浅拷贝和临时对象