0

Here is my situation:

I have a class like this:

class JSON {
private:
std::vector<JSON> children;
...
public:
JSON& operator[](std::string propertyName);
...
}

How are move semantics applied? Does it make any difference if they are used?

I feel like if I do

std::swap(json["hello"], json["world"])

The swap of the 2 JSON& objects will be as ineffective as a deep copy, because there are no pointers involved in the JSON class.

From what I understand, if I wanted to use Move Semantics efficiently, I would need to have a pointer to my vector of children to std::move that pointer effortlessly, am I correct in saying that if my class has no pointers, it is useless to std::swap?

Does std::swap do this in my case?

temp = json1.children;           // by value? deep copy of std::vector?
json1.children = json2.children; // by value? deep copy of std::vector?
json2.children = temp;           // by value? deep copy of std::vector?

Here is a quick bit of context: I am receiving text over socket, I turn the text into a JSON, I get the type of the message with json["type"] and then my goal is to send a std::shared_ptr<const JSON> containing json["data"] to subscribers subscribed to that message type. So I did this:

JSON* dummy = new JSON();
std::swap(*dummy, json["data"]);
std::shared_ptr<const JSON> dataPtr(dummy);

If I don't do this and I only do std::shared_ptr<const JSON>(&json["data"]), both the shared pointer and JSON going out of scope will try to free json["data"] (program fails). And if you do std::make_shared<JSON>(json["data"]) you call the copy constructor with a JSON&, which makes a deep copy.

I hope my intention was clear, I'm just trying to avoid deep copying either JSON or std::vector<JSON>

dodekja
  • 537
  • 11
  • 24
Bruno CL
  • 149
  • 9
  • 3
    `std::vector` already has move semantics. It generally just needs to `swap` 3 pointers or a pointer and 2 integers internally. You don't need to implement move semantics yourself. The compiler generated move constructor and assignment operators will work just fine in this case. – François Andrieux Apr 30 '21 at 20:58
  • One way would be to de-couple the JSON allocation and storage from the JSON object. A `Document` struct could store all objects and store information about the structure of the document (can be completely flat) and use `JSON` as a user friendly view on the document. – Guillaume Racicot Apr 30 '21 at 21:06

1 Answers1

3

From what I understand, if I wanted to use Move Semantics efficiently, I would need to have a pointer to my vector of children to std::move that pointer effortlessly, am I correct in saying that if my class has no pointers, it is useless to std::swap?

No that's not true in general, the standard library container classes will have move constructors that do what you want as long as you know how to invoke them, which brings us to the second part of your question:

Does std::swap do this in my case?

I know it can be really frustrating to get the answer "it depends", but as with most things in C++, it really depends.

To answer your question in the most general way possible, yes std::swap() will probably do what you want most of the time, especially if you're just working with standard library container classes. Where things get weird (and where I don't have enough information to give you a complete answer) is that you've defined your own class, and only part of it is shown here. The devil is in the details, so the actual behavior of the program will depend on what's in those ellipses.

In general when you're trying to understand what to expect with copy/move behavior, you really need to think in terms of constructors. Assuming you're using a "modern" (i.e. post-11 version) of C++, the std::swap() function is going to look something roughly like this:

template<typename T> void swap(T& t1, T& t2) {
    T temp = std::move(t1); // or T temp(std::move(t1));
    t1 = std::move(t2);
    t2 = std::move(temp);
}

See also this related post. More concretely for your example, the template instantiation will look something like this:

void swap(JSON& t1, JSON& t2) {
    JSON temp = std::move(t1); // or T temp(std::move(t1));
    t1 = std::move(t2);
    t2 = std::move(temp);
}

Keep in mind that std::move() is really just a fancy way to cast an lvalue reference to an rvalue reference with some corner case handling. The function itself doesn't do anything, it's a means to tell the compiler how to perform overload resolution.

So now the question becomes: what happens when the compiler needs to construct a JSON object from an rvalue reference to an object type JSON? The answer to this question depends on what constructors are available on the class, some of which may be implicitly generated by the compiler. See also this post.

The compiler will pick the best fitting constructor for the operation, which could be an implicit one, and depending on what you've declared on class, may not actually be a move constructor as explained in this example. To stay away from falling into that trap, you need to know that an rvalue reference can bind to a const lvalue reference, so a copy constructor with the following signature:

    JSON(const JSON &);

Is a valid overload candidate for the left hand side of std::move() operation in some cases. This is probably why you sometimes hear people saying that std::move() "isn't actually moving anything", or it's "still just copying".

So where does all of this leave your code? Basically if you have no user-declared constructors, and you're letting the compiler do it for you, then std::swap is probably going to move memory on all of your members the way you want. As soon as you start declaring your own constructors, the story gets more complicated and we have to talk specifics.

As a small postscript here, do you really need to use swap() at all? It looks like you're just trying to construct a shared_ptr to an object that's been initialized with the contents of another object. This would probably be a slightly simpler approach:

  std::shared_ptr<const JSON> outPtr = std::make_shared<JSON>(std::move(json["data"]));

This will construct an object of type JSON using a move constructor (assuming it's the best overload candidate given the caveats I mentioned) and return a shared_ptr to it.

Jon Reeves
  • 2,426
  • 3
  • 14
  • I will try to ```std::move(json["data"])``` but I thought move would free the memory there, messing up the json destructor also freeing that memory – Bruno CL May 01 '21 at 10:10
  • 1
    @BrunoCL when you do that it will indeed move out `json["data"]` into the thing you'll assign to it. But the value of `json["data"]` won't even be null, just a unspecified state. I would not do that, since it's quite dangerous. I would strongly prefer `std::exchange(json["data"], {})`. The exchange will put a default constructed value back into the `json["data"]` object. – Guillaume Racicot May 01 '21 at 15:10
  • 1
    @BrunoCL just to be perfectly clear, the move will not free the memory, semantically speaking it will change ownership. What @Guillaume said is certainly true though. After `std::move()`, the state of the original `json["data"]` object is undefined and must not be used. Ownership is taken by the object pointed to by `outPtr`, which I assumed is what you want. C++ lets you do a lot of things that would be considered dangerous for the sake of being efficient. It's up to you how you choose to use (or not use) those tools. – Jon Reeves May 02 '21 at 22:55