左值右值
左值、右值的完整概念如下
- 分为广义的左值、右值
- 广义左值包括 lvalue、xvalue
- 右值包括:xvalue、prvalue
也就是:
- 一个 lvalue 是通常可以放在等号左边的表达式,左值
- 一个 rvalue 是通常只能放在等号右边的表达式,右值
- 一个 glvalue 是 generalized lvalue,广义左值
- 一个 xvalue 是 expiring value,将亡值
- 一个 prvalue 是 pure rvalue,纯右值
prvalue包括
1
2
3
4
5
6
7
8
|
42
true
int foo(); foo()
int a{}, b{}; // lvalue
a + b; //prvalue
a++;
double{}; //prvalue
std::vector<database>();
|
lvalue 包括
1
2
3
4
5
6
7
8
9
10
11
12
|
"hello"
int a{}; //lvalue
int& get() { return a; }
get(); // lvalue
++a;
std::cout << 42;
int a[4]{}; // lvalue
a[2]; // lvalue
int foo();
int &&a { foo() }; // lvalue
int &&a{ 77 }; // lvalue
int &b{ a }; // lvalue
|
xvalue 包括
1
2
3
4
5
6
7
8
|
bool b{ true }; // lvalue
std::move(b); // xvalue
static_cast<bool&>(b); // xvalue
int&& foo();
foo(); // xvalue
using arr = int[2];
arr{}; // prvalue
arr{}[0]; // xvalue
|
根据可移动、不可移动,以及有么有标识符
可以将 rvalue、lvalue划分成 四个象限
value category 值类别,将的是左值、右值这些
value type 值类型,将的是变量是代表实际数值、还是引用另外一个数值
C++ 中所有原生类型、枚举、结构体、联合,类都是值类型,只有 引用 &、指针 * 是引用类型
生命周期扩展
一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include <stdio.h>
class shape {
public:
virtual ~shape() {}
};
class circle : public shape {
public:
circle() { puts("circle()"); }
~circle() { puts("~circle()"); }
};
class triangle : public shape {
public:
triangle() { puts("triangle()"); }
~triangle() { puts("~triangle()"); }
};
class result {
public:
result() { puts("result()"); }
~result() { puts("~result()"); }
};
result
process_shape(const shape& shape1,
const shape& shape2)
{
puts("process_shape()");
return result();
}
int main()
{
puts("main()");
process_shape(circle(), triangle());
puts("something else");
}
|
一个临时对象会在包含这个临时对象的完整表达式估值完成后、按生成顺序的逆序被销毁
结果:
1
2
3
4
5
6
7
8
9
|
main()
circle()
triangle()
process_shape()
result()
~result()
~triangle()
~circle()
something else
|
使用生命周期延长后:
1
2
|
result&& r = process_shape(
circle(), triangle());
|
结果:
1
2
3
4
5
6
7
8
9
|
main()
circle()
triangle()
process_shape()
result()
~triangle()
~circle()
something else
~result()
|
但是 这个规则只 对 prvalue 有效,对于 xvalue 无效
如果由于某种原因,prvalue 在绑定到引用以前已经变成了 xvalue,那生命期就不会延长
移动语义的例子
设计要点
- 对象应该有分开的拷贝构造和移动构造函数(除非你只打算支持移动,不支持拷贝——如 unique_ptr)
- 对象应该有 swap 成员函数,支持和另外一个对象快速交换成员
- 在你的对象的名空间下,应当有一个全局的 swap 函数,调用成员函数 swap 来实现交换。支持这种用法会方便别人(包括你自己在将来)在其他对象里包含你的对象,并快速实现它们的 swap 函数
- 实现通用的 operator=
- 上面各个函数如果不抛异常的话,应当标为 noexcept。这对移动构造函数尤为重要
一个具体例子,包含包括拷贝构造函数,移动构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
smart_ptr(const smart_ptr& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_
->add_count();
shared_count_ =
other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
|
支持 swap 的成员函数
1
2
3
4
5
6
7
|
void swap(smart_ptr& rhs) noexcept
{
using std::swap;
swap(ptr_, rhs.ptr_);
swap(shared_count_,
rhs.shared_count_);
}
|
支持 swap 的全局函数
1
2
3
4
5
6
|
template <typename T>
void swap(smart_ptr<T>& lhs,
smart_ptr<T>& rhs) noexcept
{
lhs.swap(rhs);
}
|
operator= 成员函数
1
2
3
4
5
6
|
smart_ptr&
operator=(smart_ptr rhs) noexcept
{
rhs.swap(*this);
return *this;
}
|
对于本地变量返回的优化
对于这段
1
2
3
4
5
|
X foo() {
X x;
// perhaps do something to x
return x;
}
|
foo() 是被调用者,返回 x
而编译器会做优化,将 x 直接创建到 调用者的栈上
如果使用了这种写法
1
2
3
4
5
6
|
X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}
|
反而会阻止了编译器优化
引用坍缩和完美转发
引用折叠的规则
- A& & becomes A&
- A& && becomes A&
- A&& & becomes A&
- A&& && becomes A&&
比如这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
void foo(const shape&)
{
puts("foo(const shape&)");
}
void foo(shape&&)
{
puts("foo(shape&&)");
}
void bar(const shape& s)
{
puts("bar(const shape&)");
foo(s);
}
void bar(shape&& s)
{
puts("bar(shape&&)");
foo(s);
}
int main()
{
bar(circle());
}
|
输出为:
1
2
|
bar(shape&&)
foo(const shape&)
|
首先调用到了 void bar(shape&& s) 这个函数,s 是一个右值引入,当调用 s 的时候,会被当做左值处理
所以会继续调用到 void foo(const shape&) 的函数
如果想让 bar 调用右值引用的那个 foo 的重载,可以改成:
1
2
3
|
foo(std::move(s));
// 另一种写法
foo(static_cast<shape&&>(s));
|
很多标准库里的函数,连目标的参数类型都不知道,但我们仍然需要能够保持参数的值类别:左值的仍然是左值,右值的仍然是右值
使用 std::forward 后可以改成:
1
2
3
4
|
template <typename T>
void bar(T&& s) {
foo(std::forward<T>(s));
}
|
输入和输出:
1
2
3
4
5
6
7
8
|
// 假设代码如下:
circle temp;
bar(temp);
bar(circle());
// 输出结果
foo(const shape&)
foo(shape&&)
|
std 中的源码
删除引用
1
2
3
4
5
6
7
8
9
10
11
|
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
|
std::move
1
2
3
4
|
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
|
解释:
- constexpr typename std::remove_reference<_Tp>::type&&,这个是返回类型
- 关键的是调用了这句:static_cast<typename std::remove_reference<_Tp>::type&&>(__t)
remove_reference 这个结构体有三个
当调用了 remove_reference 后,无论之前是什么情况,都会变成普通的版本,& 或者 && 都会被删除
删除了引用之后,又在后面加了两个 &, 也就是 type&& 这段,相当于不论传入的是什么,都给变成了 &&
之后,通过 static_cast,强行将 __t 转为 右值引用
也就是强制把一个 左值,强转成 右值
还有一点值得注意,函数 括号后面跟了 noexcept,表示不会抛出异常
forward
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
|
解释
- forward 有两个版本,可以接收 & 左值、以及 && 右值的参数
- 对于左值,将引用删除,然后强制转换 _Tp&&,因引用折叠,最后会强制转换成左值
- 对于右值,会强制转换成右值
- 函数包含了 noexcept,表示不会抛出异常;
- constexpr 表示 在编译期间做计算
参考
- cppreference.com, “Value categories”
- cppreference.com, “值类别”
- Anders Schau Knatten, “lvalues, rvalues, glvalues, prvalues, xvalues, help!”
- Jeaye, “Value category cheat-sheet”
- value-category-cheatsheet.pdf
- Thomas Becker, “C++ rvalue references explained”
- Herb Sutter, “GotW #88: A candidate for the ‘most important const’