C++ 居然可以给右值赋值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

struct A {

};

A f(){
return {};
}

int main() {
A a;
f() = a;
}

这段代码没有报错,这里的 f() 应该是一个纯右值表达式,因为它是一个返回类型是非引用的函数调用

类实际上会隐式声明一个复制赋值运算符,如果把它 delete 掉,还能成功运行吗?

1
A& operator=(cosnt A&) = delete;

就不能了,因为 右值不能用作内建赋值运算符及内建复合赋值运算符的左操作数。 例子中的 f() = a的 = 不是内建的函数

移动语义相关

1
2
3
4
5
6
7
8
#include <iostream>

int test(int&&) {}

int main(){
int &&a = 1;
test(a); // 报错
}

局部声明的右值引用是某些情况下可以视为左值;这里需要 test(std::move(a));.


移动语义与所有权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>

int main(){
// 所有权
std::unique_ptr<int> p;
std::coutt << (bool)p << '\n'; // 0
p = std::make_unique<int>(6);
std::coutt << (bool)p << '\n'; // 1

//auto p2{ p }; 报错,因为unique_ptr独占,删除了一些东西。unique_ptr(const unique_ptr&] = delete; unique_ptr& operator=(const unique_ptr&) = delete;
auto p2{ std::move(p) };
std::coutt << (bool)p << '\n'; // 0
std::shared_ptr<int>p3{ new int{} };
std::cout << p3.use_count() << \n'; // 1
auto p4{ p3 };
std::cout << p3.use_count() << n'; // 2
auto p4 { std::move(p3) }:
std ::cout << p3.use_count() << '\n'; // 0;
}

至此,你就可以理解移动语义了

那么被移动的对象还能不能继续使用?

除非另有规定,此类移动对象应置于有效但未指定的状态。
比如移动了一个vector

1
2
3
4
void foo(vector<int>& a, vector<int>& b){
a = move(b); // Move
b.push_back(1); // 没有指定push_back之后的最后一个元素1的前身是什么元素
}

b 被移动后,其数据为空吗?还是会和 a 交换?这些都有可能,是不确定的,所以不应直接访问b,应主动清空其数据再访问。

1
2
3
4
5
void foo(vector<int>& a, vector<int>& b){
a = move(b); // Move
b.clear();
b.push_back(1); // Right
}

这样才是安全的。

被移动的对象既可以读取自己的数据成员的值,也可以调用自己的成员函数(如果是类类型)(如果是无前提的话)。
特别说明:一个成员函数在对象任何状态下进行调用都返回合法的值,这个函数就是无前提的。
我们拿vector的begin()和front()成员函数进行说明。

  • front()的调用合法依赖容器不为空(不然就是ub)
  • begin()并不依赖

那么你是否认为,被 std::move 后的 vector 调用 front() 成员函数就一定是 ub 呢?

显然不是,因为移动以后状态不一定是空的。
我们再用std::thread 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <fmt/core.h>
#include <iostream>
#include <memory>
#include <thread>

int main(){
std::thread t{[] {fmt::print("乐\n"); } };
std::thread t2 = std::move(t);
t = std::move(t2);

fmt::print("t:{} t2:{}", t.joinable(), t2.joinable());

t.join();
}

以上代码没有问题,即使t已经被移动过,但依然可以t = std::move(t2) 这是理所应当的。

包括对joinable()的调用,也毫无问题。顺带一提,智能指针和std::thread 在C++有特殊指定,被移动后的状态为空。

如果是平凡类型的话(包括平凡类类型),一切的移动构造,移动赋值之类的操作,等价于复制。 int b = std::move(a) 等价于 int b = a,