Why does rvalue object does not get moved to function with rvalue parameter?

I was wondering why std::move to a function with an rvalue parameter was not actually moving anything, but passing by reference instead?

Especially when I know it works for constructors.

I was running the following code:

#include <memory>
#include <iostream>

void consume_ptr(std::shared_ptr<int> && ptr) {
    std::cout << "Consumed " << (void*) ptr.get() << std::endl;
}

int main(int argc, char ** argv)
{
    std::shared_ptr<int> ptr = std::make_shared<int>();
    consume_ptr(std::move(ptr));
    if (ptr) {
        std::cout << "ptr should be moved?" << std::endl;
    }
    return 0;
}

The output is:

ptr should be moved?

According to everything I've read, the std::shared_ptr should have been moved inside the function, meaning that the object ptr itself would hold nullptr after moving it into consume_ptr, but it doesn't!

I tried it with some custom class of mine with logging, and it looks like the move constructor is never even called. It's reproducible under every compiler and optimization level.

Can anyone clear this up for me please?


Solution 1:

std::move by itself does not actually move anything. That is to say, std::move alone does not invoke any move constructors or move-assignment operators. What it actually does is effectively cast its argument into an rvalue as if by static_cast<typename std::remove_reference<T>::type&&>(t), according to cppreference.

In order for any moving to actually happen, the moved object must be assigned to or used in the move-initialization of something else. For example, it could be used to initialize a member.

void something::consume_ptr(std::shared_ptr<int> && ptr) {
    this->ptr = std::move(ptr);
    std::cout << "Consumed " << (void*) ptr.get() << std::endl;
}

However, one way to make your pointer get moved without being assigned to anything is to simply pass it by value, causing your pointer to be moved into the parameter.

void consume_ptr(std::shared_ptr<int> ptr) {
    std::cout << "Consumed " << (void*) ptr.get() << std::endl;
}

This way can actually be more useful than the rvalue way if you're going to end up assigning the parameter to something, because it allows you to pass stuff in by copy, too, and not just by move.

void consume_ptr_by_rvalue(std::shared_ptr<int> && ptr);
void consume_ptr_by_value(std::shared_ptr<int> ptr);

void do_stuff() {
    std::shared_ptr<int> x = /*...*/;
    std::shared_ptr<int> y = /*...*/;

    // consume_ptr_by_rvalue(x); // Doesn't work
    consume_ptr_by_rvalue(std::move(y)); // Risk of use-after-move

    std::shared_ptr<int> z = /*...*/;
    std::shared_ptr<int> w = /*...*/;

    consume_ptr_by_value(z);
    consume_ptr_by_value(std::move(w)); // Still risk, but you get the idea
    consume_ptr_by_value(make_shared_ptr_to_something()); // Can directly pass result of something
}

Solution 2:

Here:

void consume_ptr( std::shared_ptr<int>&& ptr )
{
    std::shared_ptr<int> new_ptr { ptr }; // calling copy ctor

    std::cout << "Consumed " << ( void* ) new_ptr.get( ) << '\n';
}

No move happens there. No call to a move ctor or a move assignment operator. That's just a simple pass by rvalue reference there.

Output:

Consumed 0x20d77ebe6a0
ptr should be moved?

Now take a look at this:

void consume_ptr( std::shared_ptr<int>&& ptr )
{
    std::shared_ptr<int> new_ptr { std::move(ptr) }; // calling move ctor

    std::cout << "Consumed " << ( void* ) new_ptr.get( ) << '\n';
}

int main( )
{
    std::shared_ptr<int> ptr { std::make_shared<int>( ) };
    consume_ptr( std::move(ptr) );

    if ( ptr )
    {
        std::cout << "ptr should be moved?" << std::endl;
    }
}

Output:

Consumed 0x21160bbdf40

See. The move operation happened. And the if's body did not run.