c++c++11memory-managementstdmove-semantics

Using std::move with Constructor(Type t) or Constructor(Type&& t). What's the difference?


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.


Solution

  • 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 use std::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.