I'm working on a simple JSON parser for fun, and I've got my value type:
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 value\n");
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;
}
};
I've written proper copy/move constructors and an assignment operator. My issue is that, when running a particular benchmark, the parser takes about 40ms. Trying to optimize a bit, I commented out the copy constructor to make sure I'm not making any unecessary copies. Sure enough, my code still compiles, indicating the move constructor is sufficient, and it's ~25% faster!
Instrumenting the copy constructor, I can see it is indeed being called, but as I've shown, the move constructor is sufficient.
So, my question is, in what contexts would a copy constructor be preferred over the move constructor, and how can I find where that's happening?