Last Updated: 2023-12-19 00:31:19 Tuesday
-- TOC --
前面总结过Copy Semantics,它实现了将一个对象所有资源copy一份到另一个对象的功能,结果是两个对象状态一致。但copy很多时候是低效的,C++同时还可以提供了Move Semantics,即将一个对象的所有资源,高效地move到另一个对象中去。
这部分叫做Value Category,现在我只能非常简单的理解一下。
lvalue:left value
,可以简单理解为有名称的对象都是lvalue;变量都是lvalue;
rvalue:right value
,可以简单理解为非lvalue;临时的,没名称的;literacy;
实现move操作的一般做法是,overload一个以rvalue为入参的constructor,程序员自己实现move逻辑。
C++语法规定,&
表达的reference是lvalue,&&
表达的reference是rvalue。
下面这段测试代码,实现了一个move constructor:
#include <cstdio>
#include <cstring>
#include <utility>
struct box {
char *p;
box() = default;
box(const char *content) {
int len = strlen(content);
if (len != 0) {
p = new char[len+1]{};
strcpy(p, content);
}
}
void show() {
printf("%s\n", p?p:"(null)");
}
// copy constructor
box(const struct box& a) {
printf("copy constructor\n");
int len = strlen(a.p);
if (len != 0) {
p = new char[len+1]{};
strcpy(p, a.p);
}
}
// move constructor
box(struct box&& a) noexcept {
printf("move constructor\n");
p = a.p;
a.p = NULL;
}
~box() {
delete[] p;
}
};
int main(void) {
struct box a{"abcdefg"};
struct box b{std::move(a)};
b.show();
a.show();
return 0;
}
对象a是lvalue(有名字),通过std:move
将其转换成rvalue,在构建对象b的时候,会调用move constructor。如果不使用std::move,直接传入对象a,此时应该是调用copy constructor,但是上面测试代码没有实现,编译器会报错。
关键词
noexcept
申明这个函数不会throw,资料上说,编译器常常会放弃调用没有noexcept申明的move constructor,转而使用copy constructor,据说copy更安全。具体参考:详解C++的异常处理
下面是move assignment的代码:
// move assignment
struct box& operator=(struct box&& a) noexcept {
if (this == &a)
return *this;
p = a.p;
a.p = NULL;
return *this;
}
--snip--
int main(void) {
struct box a{"abcdefg"};
struct box b{};
b = std::move(a);
b.show();
a.show();
struct box c{std::move(b)};
c.show();
b.show();
a.show();
return 0;
}
下面是测试代码的运行结果:
$ g++ -Wall -Wextra test_move.cpp -o test_move
$ ./test_move
abcdefg
(null)
move constructor
abcdefg
(null)
(null)
重要的细节
对象被move之后,它拥有的资源就转移给其它对象了,逻辑上看,这个对象就不能再正常使用了!
测试1:识别lvalue或rvalue
#include <cstdio>
#include <string>
using namespace std;
struct xyz {
string what;
xyz(const string& str) {
printf("lvaule init\n");
what = str;
}
xyz(const string&& str) {
printf("rvalue init\n");
what = str;
}
};
int main(void) {
xyz x{"123123"}; // rvaule
string s{"abcdefg"};
xyz y{s}; // lvalue
xyz z{string{"hahah..."}}; // rvalue
string&& s2{"9999999"}; // syntax ok, but no use
xyz x2{s2}; // lvalue
return 0;
}
s2的申明是一种语法冗余,因为"9999999"是rvalue,那就不能用单个&
,用两个或一个都不用,都OK,效果一样,得到的s2,是个lvalue。
运行结果符合预期:
$ g++ -Wall -Wextra tr.cpp -o tr
$ ./tr
rvalue init
lvaule init
rvalue init
lvaule init
测试2:测试函数返回一个local object
#include <cstdio>
#include <string>
#include <iostream>
using namespace std;
struct xyz {
xyz(): p{} {}
xyz(const string& str) {
p = new string{str};
}
void show() {
cout << *p << endl;
}
xyz(xyz&& x) {
printf("move constructor...\n");
p = new string{*x.p};
delete x.p;
x.p = nullptr;
}
xyz& operator=(xyz& x) {
printf("copy assignment...\n");
delete p;
p = new string(*x.p);
delete x.p;
x.p = nullptr;
return *this;
}
xyz& operator=(xyz&& x) {
printf("move assignment...\n");
delete p;
p = new string(*x.p);
delete x.p;
x.p = nullptr;
return *this;
}
~xyz() {
delete p;
}
private:
string* p;
};
xyz create_xyz(void) {
string s{"abcdefg"};
xyz y{s};
return y; // return a local xyz object.
}
int main(void) {
xyz x;
x = create_xyz();
x.show();
xyz y{std::move(x)};
y.show();
// The default copy constructor is deleted by default.
//xyz z{x};
//z.show();
return 0;
}
由于y支持move,因此,move assignment is triggered by a returned local y from C++11。y在其scope内,是lvalue。可以总结概括一下:return a local lvalue which is move-capable is just OK!
运行结果:
$ g++ -Wall -Wextra tr.cpp -o tr
$ ./tr
move assignment...
abcdefg
move constructor...
abcdefg
这段测试代码,依然有谜题没有解开,如果写成xyz x{create_xyz()};
,真不知道调用的是哪个constructor?打印p的地址看是copy,但copy constructor已经默认被delete了,显示地delete也是一样。打印对象地址,也是一样的...?
关于这个问题答案,参考:RVO和NRVO优化机制
std::move
将lvalue转换成rvalue,在头文件<utility>
中。
The C++ committee probably should have named std::move as std::rvalue, but it’s the name we’re stuck with. The std:move function doesn’t actually move anything—it casts.
std::swap
交换两个同类型对象所包含的所有属性。(在C语言中,我们常常实现swap,入参是指针,std::swap是升级版,是个template function,适合所有支持move的对象)
据说这是std::swap的源码:
template<typename _Tp>
inline void
swap(_Tp& __a, _Tp& __b)
#if __cplusplus >= 201103L
noexcept(__and_<is_nothrow_move_constructible<_Tp>,
is_nothrow_move_assignable<_Tp>>::value)
#endif
{
// concept requirements
__glibcxx_function_requires(_SGIAssignableConcept<_Tp>)
_Tp __tmp = _GLIBCXX_MOVE(__a);
__a = _GLIBCXX_MOVE(__b);
__b = _GLIBCXX_MOVE(__tmp);
}
其中_GLIBCXX_MOVE()
是一个宏定义#define _GLIBCXX_MOVE(__val) std::move(__val)
。
std::swap核心其实就三行代码,自己写可以这样(教科书中的写法):
template<typename T>
inline void swap(T &a,T &b) noexcept
{
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
就是执行一次move constructor和两次move assignment。
本文链接:https://cs.pynote.net/sf/c/cpp/202209011/
-- EOF --
-- MORE --