复制构造函数优先于移动构造函数

Copy constructor being preferred over move constructor?

本文关键字:构造函数 移动 优先于 复制      更新时间:2023-10-16

为了好玩,我正在开发一个简单的JSON解析器,我的值类型是:

typedef enum {
JSON_NULL,
JSON_NUMBER,
JSON_STRING,
JSON_ARRAY,
JSON_OBJECT,
JSON_BOOLEAN
} json_type_t;
// datatype for json value
struct json_value {
using arr_type = vector<json_value>;
using obj_pair = pair<string, json_value>;
using obj_type = unordered_map<string, json_value>;
// constructors 
json_value()
: type(JSON_NULL) {}
json_value(json_type_t type)
: type(type) {
switch(type) {
case JSON_STRING:  str = new string;   break;
case JSON_ARRAY:   arr = new arr_type; break;
case JSON_OBJECT:  obj = new obj_type; break;
default:
break;
}
}
// copy construct
json_value(const json_value& other) {
printf("copying json valuen");
if (other.type != JSON_NULL) {
type = other.type;
switch(type) {
case JSON_NULL:                                    return;
case JSON_NUMBER:  num = other.num;                return;
case JSON_BOOLEAN: val = other.val;                return;         
case JSON_STRING:  str = new string  (*other.str); return;
case JSON_ARRAY:   arr = new arr_type(*other.arr); return;
case JSON_OBJECT:  obj = new obj_type(*other.obj); return;
}
}
}
// move construct
json_value(json_value&& other) {
type = other.type;
switch(type) {
case JSON_NULL:                     break;
case JSON_NUMBER:  num = other.num; break;
case JSON_BOOLEAN: val = other.val; break; 
case JSON_STRING:  str = other.str; other.str = nullptr; break;
case JSON_ARRAY:   arr = other.arr; other.arr = nullptr; break;
case JSON_OBJECT:  obj = other.obj; other.obj = nullptr; break;
}
}
// assignment operator copy/swap idiom
json_value& operator =(json_value other) {
destroy();
type = other.type;
switch(type) {
case JSON_NULL:                     break;
case JSON_NUMBER:  num = other.num; break;
case JSON_BOOLEAN: val = other.val; break; 
case JSON_STRING:  str = other.str; other.str = nullptr; break;
case JSON_ARRAY:   arr = other.arr; other.arr = nullptr; break;
case JSON_OBJECT:  obj = other.obj; other.obj = nullptr; break;
}
return *this;
}
// destructor
~json_value() {
destroy();
}
// type of value and union to hold data
json_type_t type = JSON_NULL;
union { 
bool      val;
double    num;
string   *str;
arr_type *arr;
obj_type *obj;
};
private:
// cleanup our memory
void destroy() { 
switch(type) {
case JSON_NULL:    break;
case JSON_NUMBER:  break;
case JSON_BOOLEAN: break; 
case JSON_STRING:  delete str; break;
case JSON_ARRAY:   delete arr; break;
case JSON_OBJECT:  delete obj; break;
}
type = JSON_NULL;
}
};

我已经编写了正确的复制/移动构造函数和赋值运算符。我的问题是,当运行一个特定的基准测试时,解析器大约需要40ms。为了进行一些优化,我注释掉了副本构造函数,以确保我没有制作任何不必要的副本。果不其然,我的代码仍然可以编译,这表明move构造函数已经足够了,的速度快了25%!

通过检测复制构造函数,我可以看到它确实被调用了,但正如我所展示的,移动构造函数就足够了。

所以,我的问题是,在什么情况下,复制构造函数比移动构造函数更受欢迎,我如何才能找到发生这种情况的地方?

标准容器都试图具有强异常保证,这意味着如果抛出异常,就好像什么都没发生一样。

std::vector为例。为了保持这种保证,只有在保证移动不会抛出的情况下,它才能使用move构造函数。考虑向量需要调整缓冲区大小的情况:

d // new element to push_back
[a][b][c] // old, filled buffer
[ ][ ][ ][ ][ ][ ] // new, empty buffer

将新元素移动到位不是问题,即使它抛出,因为我们仍然可以使用旧的缓冲区:

[a][b][c]
[ ][ ][ ][d][ ][ ]

但是,当我们将旧缓冲区的元素移动到新缓冲区时,如果我们在中间抛出,会发生什么?

[ ][#][c]
[a][#][ ][d][ ][ ]

我们不知道b抛出时所处的状态,那么我们如何重新创建旧状态呢?即使我们能够复活b,我们也不能只是把之前的元素移回去,因为移动这些元素也可能会投掷。

如果我们退回到一个副本,我们可以随时通过丢弃新的缓冲区来退回。

因此,为了保持强异常保证,除非移动构造函数是noexcept,否则std::vector不能移动。


将移动操作声明为noexcept对于标准容器使用它们是必要的。大多数时候,move构造函数和move赋值可以是noexcept,所以当它们是时,就这样声明它们

json_value(json_value&& other) noexcept {
// ...
}
json_value& operator=(json_value&& other) noexcept {
// ...
}