左值右值

左值、右值的完整概念如下

  • 分为广义的左值、右值
  • 广义左值包括 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 表示 在编译期间做计算

参考