Search code examples
c++rvalue-reference

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

  • 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
    }