Zoso
Zoso

Reputation: 3465

Moving from a const rvalue

I came across some code that used const rvalue references to a std::function in a function argument which was passed in a lambda. The confusing part was that it then had a std::move call on this passed in argument. Something like this:

using CallbackFn = std::function<void()>;
using AnotherCbFn = std::function<void(int)>;

void bar(AnotherCbFn&& cb) {
    // doSomething();
}

void foo(CallbackFn const&& cb) {
    // Some code
    bar([ x = std::move(cb) /* <-- What's this? */](int value){
        x();
    });
}

void baz() {
    foo([](){
        // doSomethingMore();
    });
}

What's the purpose of passing in const-value references and then invoking std::move on them? So I tried a simpler code snippet to see what happens in such cases

#include <utility>
#include <string>
#include <cstdio>
#include <type_traits>

struct Foo {
    Foo() = default;

    Foo(Foo&& o) {
        str = std::move(o.str); // calls the move assignment operator
        std::printf("Other [%s], This [%s]\n", o.str.data(), str.data());
    }

    Foo(Foo const&& o) {
        str = std::move(o.str); // calls the copy assignment operator
        std::printf("Other [%s], This [%s]\n", o.str.data(), str.data());
    }

    private:
    std::string str = "foo";
};

template <typename T>
void f(T&& x) {
    if constexpr(std::is_const_v<T>) {
        std::printf("Const rvalue\n");
        auto temp = std::move(x);
    } else {
        std::printf("non-const rvalue\n");
        auto temp = std::move(x);        
    }
}

Foo const getConstRvalue() {
    return Foo();
}

Foo getNonConstRvalue() {
    return Foo();
}

int main() {
    f(getConstRvalue());
    f(getNonConstRvalue());
}

which yielded the output:

Const rvalue
Other [foo], This [foo]
non-const rvalue
Other [], This [foo]

Checking up the assembly at godbolt(here) confirms what's happening. The Foo(const&&) invokes the copy-assignment operator of std::string:

call std::__cxx11::basic_string<char, std::char_traits, std::allocator >::operator=(std::__cxx11::basic_string<char, std::char_traits, std::allocator > const&)

whilst Foo(Foo&&) invokes the move assignment operator of std::string:

call std::__cxx11::basic_string<char, std::char_traits, std::allocator >::operator=(std::__cxx11::basic_string<char, std::char_traits, std::allocator >&&)

I think (please correct me!) that a const-lvalue function argument can bind to a const rvalue argument as well(along with a non-const rvalue, const lvalue, and non-const lvalue), which is why there's a copy in the case of Foo(const&&) since a const-rvalue to std::string can't bind to a non-const rvalue in the move assignment operator.

So, what's the purpose of passing const rvalue reference and then invoking std::move on it since calling std::move usually implies that the value is not supposed to be used after that and in this case, actually a copy is involved instead of the desired move semantics? Is there some subtle language mechanism at play?

Upvotes: 4

Views: 615

Answers (1)

Damir Tenishev
Damir Tenishev

Reputation: 3272

std::move moves nothing, it just reinterprets lvalue (reference to rvalue cb) to the rvalue which is expected by some bar function which you forgot to show in your code snippet.

I suspect it looks like:

void bar(CallbackFn const&& cb) {
  ...
}

Upvotes: 2

Related Questions