I'm studying std::move
semantics and I would like this to be clarified to me:
Say I have:
// Message.h
class Message
{
private:
std::array<uint8_t, BUFFER_SIZE> buffer;
std::queue<Packet> packets;
public:
Message(Packet p)
{
packets.push(std::move(p));
}
}
Now I have:
Packet p;
p << data; // put data into the packet
Message m(p) // wrap the packet in a message.
So this code generates unnecessary copies for my use case, because I will never use p
ever again. So I'd like to use the std::move
semantics to avoid every copy possible.
What I don't understand is the following:
If I do:
Packet p;
Message m(std::move(p));
While the constructor of Message
is still
Message(Packet p) // p is passed by value
p
is passed by value, so is it still copied?
Also both of these work:
Packet a;
Packet b;
Message mess1(std::move(a)); // no errors
Message mess2(b); // no errors
But I fail to see the difference. The first one prevents the copy (even with that constructor) and the seconds allows the copy? Or not?
While If I modify the constructor so that:
Message(Packet&& c)
Am I right to assume that in this case no copies will ever be made and I'm basically forcing the programmer to use std::move(a)
otherwise the program won't compile?
I tried to do multiple tests with objects and try to understand the difference but I couldn't.
While the constructor of Message is still
Message(Packet p) // p is passed by value
p
is passed by value, so is it still copied?
If you pass a Packet
lvalue to that constructor like you've shown in your question, it'll be copied.
While If I modify the constructor so that:
Message(Packet&& c)
Am I right to assume that in this case no copies will ever be made and I'm basically forcing the programmer to usestd::move(a)
otherwise the program won't compile?
That constructor will only accept rvalues, yes, but if that prevents copies depends entirely on how Packet
is implemented. If it has a move constructor, that will be used. If that constructor does something that prevents a deep copy depends on how it's implemented.
If you had a Message
that supported perfect forwarding
class Message {
private:
std::queue<Packet> packets;
public:
template <class... Args>
requires std::constructible_from<Packet, Args...>
explicit Message(Args&&... args) {
packets.emplace(std::forward<Args>(args)...);
}
/* you probably want this too:
template <class... Args>
requires std::constructible_from<Packet, Args...>
decltype(auto) emplace(Args&&... args) {
return packets.emplace(std::forward<Args>(args)...);
}
*/
};
and Packet
had a constructor taking data
as-is, you could simply do
Message m(data);
and there would only be one Packet
instance - the one created inside packets
.