左值与右值,std::swap 与 std::move

左值与右值

C++ 的表达式不是右值(rvalue)就是左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

左值引用与右值引用

左值引用即常规引用,通过将声明符写成 &d 的形式来定义类型,引用必须被初始化,且只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

例外情况是在初始化常量引用时允许用任意表达式作为初始值。

1
2
int &refVal2;      // 错误:引用必须被初始化
int &refVal4 = 10; // 错误:引用类型的初始值必须是一个对象

右值引用就是必须绑定到右值的引用(即只能绑定到一个将要销毁的对象),通过 && 而不是 & 来获得右值引用。

返回非引用类型的函数,连同算数、关系、位以及后置递增 / 递减运算符,都生成右值。

1
2
3
4
5
6
7
int i = 42;
int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误:i * 42 是一个右值
const int &r3 = i * 42; // 正确:我们可以将一个 const 的引用绑定到一个右值上
int &&rr2 = i * 42; // 正确:将 rr2 绑定到乘法结果上
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式 rr1 是左值

std::movestd::swap

std::movestd::swap 同时存在于头文件 <algorithm><utility> 中,但是 std::move 在两个头文件中的定义不同。

std::swap

std::swap 在 C++98 标准中存在于 <algorithm>,在 C++11 标准中被移到了 <utility> 中(<algorithm> 中仍然存在)。

对于 std::swap 的一个解释是:

The committee wanted to allow you to use swap() without introducing a compile-time dependency on the large and more complex <algorithm> header file. Because swap() is so widely used, it makes sense to let you pull in its definition with as little additional baggage as possible; this will generally lead to faster compile times for files that don’t otherwise need <algorithm>. Its new home allows it to be used without introducing unneeded overhead.

1
2
3
4
5
6
7
8
9
// C++98
#include <algorithm>

template <typename T> void swap (T& a, T& b)
{
T c(a);
a = b;
b = c;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++11
#include <algorithm>

template <typename T> void swap (T& a, T& b)
{
T c(std::move(a));
a = std::move(b);
b = std::move(c);
}

template <typename T, size_t N> void swap (T (&a)[N], T (&b)[N])
{
for (size_t i = 0; i < N; ++i)
swap (a[i], b[i]);
}

using std::swap

Many components of the standard library (within std) call swap in an unqualified manner to allow custom overloads for non-fundamental types to be called instead of this generic version: Custom overloads of swap declared in the same namespace as the type for which they are provided get selected through argument-dependent lookup over this generic version.

1
2
3
4
5
void swap(ClassTest &t) noexcept
{
using std::swap;
swap(str, t.str); //交换指针,而不是 string 数据
}

在 C++ 里存在多种名字查找规则:

  1. 普通的名字查找:先在当前的作用域里查找,查找不到再到外层作用域中查找,即局部相同名字的变量或函数会隐藏外层的变量或作用域。
  2. Argument-dependent lookup (ADL)
  3. 涉及函数模板匹配规则:一个调用的候选函数包括所有模板实参推断成功的函数模板实例;候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板;如果恰好有一个函数(或模板)比其他函数更加匹配,则选择该函数;同样好的函数里对于有多个函数模板和只有一个非模板函数,会优先选择非模板函数;同样好的函数里对于没有非模板函数,那么选择更特例化的函数模板。
  4. 因为 C++ 会优先在当前的作用域里查找,所以使用 using std::swap 将标准库的 swap 模板函数名字引入该局部作用域,重载当前作用域的同名函数,隐藏外层作用域的相关声明。当经过普通的名字查找后(没有包括 ADL),如果候选函数中有类成员、块作用域中的函数声明(不包括 using 声明引入的)、其他同名的函数对象或变量名,则不启动 ADL 查找了。如果没有,则进行 ADL 查找。因此在经过普通的查找后,发现并没有匹配的函数,最后再经过 ADL 找到了标准库中的 swap 和外层作用域的 void swap(ClassTest &a, ClassTest &b) noexcept,由于后者较匹配,编译器优先选择后者。
  5. 如果 str 类型有自定义的 swap 函数,那么第 4 行代码的 swap 调用将会调用 str 类型自定义的 swap 函数。
  6. 但是如果 str 类型并没有特定的 swap 函数,那么第 4 行代码的 swap 调用将会被解析到标准库的 std::swap

std::move

标准库函数 move 可以获得绑定到左值上的右值引用,此函数定义在头文件 utility 中。

1
int &&rr3 = std::move(rr1);

移动构造函数和移动赋值运算符如果不抛出任何异常,就应该将它标记为 noexcept

如果类定义了一个移动构造函数和 / 或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。(定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的)。

<utility> 中的 std::move 返回一个右值引用,而头文件 <algorithm> 重载了这个函数,提供了应用于范围的类似行为。

1
2
3
4
5
6
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}

<algorithm> 中的 std::move 移动元素的范围:

[first, last) 范围内的元素移动到从 result 开始的范围内。

[first, last) 范围内的元素的值被转移到 result 指向的元素。调用后,范围 [first,last) 中的元素被留在一个未指定但有效的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++11
#include <algorithm>

template<typename InputIterator, typename OutputIterator>
OutputIterator move (InputIterator first, InputIterator last, OutputIterator result)
{
while (first != last)
{
*result = std::move(*first);
++result;
++first;
}
return result;
}

简单总结

movelhs 释放自己的资源,然后接管 rhs 的资源。移动之后,rhs 应该具有更少的资源(比如直接置空各种指针),移动之后需假设 rhs 除非接收其他移动,否则就是个无效的对象了;而 swap 则是交换 lhsrhs 的资源,两者都没有析构操作,交换之后两者均可用。

参考