3

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?

Zoso
  • 3,273
  • 1
  • 16
  • 27
  • Did you see this `const&&` only in one codebase or in several places? I never saw that in any book, guideline... [Apparently](https://stackoverflow.com/q/4938875/11527076) it just exists to be disabled... – prog-fh Aug 26 '21 at 21:34
  • @prog-fh This is the first time I've seen it in one codebase. I have only seen this in places to avoid resolution binding to `constT&&` such as `std::ref` etc. but never this usage of passing in a `constT&&` and then a `std::move`. – Zoso Aug 26 '21 at 21:37
  • 2
    Looks like some workaround for a buggy compiler to me. When all this stuff was brand new, there could be compiler bugs where you had to do a `T const&&` overload to get the code to build. I don't remember the details. It's been many years by now. – Nikos C. Aug 26 '21 at 21:37
  • 2
    `Foo(Foo&& o)` is a move constructor, so `Foo(Foo const&& o)` was obviously meant to be a copy constructor, and as such it should have been declared as `Foo(Foo const& o)` instead (and `move()` removed from it), using a const lvalue reference instead of a const rvalue reference. – Remy Lebeau Aug 26 '21 at 21:47
  • I think `Foo(Foo&& o) { str = std::move(o.str); }` instead of `Foo(Foo&& o) : str(std::move(o.str)) {}` (and of course the `Foo(Foo const&& o)` part) is a sign that the person who wrote this was a bit new at this. I hope the codebase isn't widely used. – Ted Lyngmo Aug 26 '21 at 21:54
  • 1
    @TedLyngmo Yeah...that's the part. It was written by a *senior developer* and reviewed by a *senior developer*...hence my question to gauge whether I missed something fundamental. – Zoso Aug 26 '21 at 21:56
  • 1
    I see ... :-) Re: _`bar(std::move(cb)); // <<-- What's this?`_ - it calls the function `bar` with a `const CallbackFn&`. – Ted Lyngmo Aug 26 '21 at 21:59
  • While it isn't literally impossible to have different behavior between `T(T&)`, `T(T&&)` and `T(const T&&)` it would be very strange. There may be some `mutable` state which can still be transferred. But, in my opinion, even this was the case, this kind of code would be so surprising and raise so many questions that it outweigh basically any other concerns and should be generally banned by whatever coding convention is being used. – François Andrieux Aug 27 '21 at 00:00
  • Having a `const` rvalue reference overload is unusual enough that the absence of a comment explaining it is basically a code defect. – François Andrieux Aug 27 '21 at 00:01

1 Answers1

2

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) {
  ...
}
Damir Tenishev
  • 1,275
  • 3
  • 14
  • Sorry for leaving out `bar()`. I've updated the question to mark the correct flow of passing lambdas around. – Zoso Aug 27 '21 at 07:27
  • @Zoso, so have my answer worked for you or (taking into account that you updated the question and didn't finally accept the answer) you still have questions to address in this scope? – Damir Tenishev Aug 28 '21 at 17:23
  • You explain what `std::move` does. My question was related to what's the use of calling `std::move` on const rvalue references when one intends to possibly *move* out the contents of the memory when using lambdas, strings etc. to save on a copy. I'm aware that `std::move` in itself does not move memory but the rvalue it generates is used as an indication at the callee site to use that memory differently, to *steal* the memory. From what I gather from the comments, it's pointless and probably unintended and as I noted in the question, forces copy semantics instead. – Zoso Aug 29 '21 at 14:17
  • @Zoso, well, how do I see the context from your message (correct me if I am wrong): you have foo function which takes const&& and some bar function that gets const&& and this is given; so you don't have control over given circumstances. In this case, could you please provide your version to call bar from foo without std::move? In case the context is different (you can change the signatures, etc.), please specify it. From you explanation at the moment I can't see what are constraints and what are not. – Damir Tenishev Aug 29 '21 at 18:30
  • 1
    The signature `void foo(CallbackFn const&& cb)` is incorrect and should be simply `void foo(CallbackFn&& cb)` since passing a const rvalue reference which is meant to be moved from later doesn't make sense. My question was not really on *how* to get things to work/compile but more on ***why*** have `T const&&` in the first place, which as is discussed in the comments and from the example I'd shared in the question, seems inappropriate in this case. I hope that clarifies things. Thank you for looking into the question though! – Zoso Aug 29 '21 at 21:03
  • Well, maybe I am the only person who is confused, but let me explain how I hear you. (1) Your post starts with "Moving"; (2) in the text you say "The confusing part was that it then had a std::move call on this passed in argument". For me your question is about rather about moving than about const. Maybe it worth some rewording? Am I got you correctly that you asking only about const&& in foo? If so, how all this relates to std::move in your question? Could you please help me to understand the question? Can you show what you want to change in the first code snippet? – Damir Tenishev Aug 29 '21 at 22:48
  • In my question, I've rehashed my actual question twice: 1) `What's the purpose of passing in const-value references and then invoking std::move on them? ` and then at the end 2)`So, what's the purpose of passing const rvalue reference and then invoking std::move on it...`. My question was about the purpose, ***why?*** is there a move on a const rvalue and does it serve any purpose since as I write `actually a copy is involved instead of the desired move semantics`, which makes it all the more damaging to have a `std::move`. I wanted to know if this is even the right way to go about it. – Zoso Aug 29 '21 at 22:55
  • I was curious if there's something that I haven't understood correctly from rvalue concepts that would explain moving from a const rvalue. I suspected it was incorrect code and the discussion in the comments confirmed my suspicions. If one sees a `std::move` on a const rvalue, it's probably not what the author of the code intended and should just pass in a plain rvalue, so that move semantics can be applied. If you still feel that my questions aren't suited to the question's text, please do suggest an edit so that it becomes more clear for you(and probably others). – Zoso Aug 29 '21 at 23:00
  • Let's make step by step. (1) I am looking into the first code snippet. It asks "/* <-- What's this? */". The question is answered. It doesn't ask anything else. I am asking again, can you please show how you want this code to look from your perspective? (2) When you ask "What's the purpose of passing in const-value references and then invoking std::move on them?" it is unclear if you stress on const-value references or on move. What's in your way? Let's start with code, what you saw wrong (I assume first code snippet) and what you wanted to see? – Damir Tenishev Aug 29 '21 at 23:27
  • For 1) I expect code to be `void foo(CallbackFn&& cb)` without the const rvalue 2)Move semantics don't apply on a const rvalue, so that's incorrect. – Zoso Aug 30 '21 at 07:21