10

I have the following (contrived) code where I have a printer class with a single print function in it and a working class that processes a string and then calls a callback function to the print function:

#include <functional>
#include <iostream>

using callback_fn = std::function<bool(std::string)>;

class printer
{
public:   
    bool print(std::string data)
    {
        std::cout << data << std::endl;
        return true;
    }
};

class worker
{
public:   
    callback_fn m_callback;
    void set_callback(callback_fn callback)
    {
        m_callback = std::move(callback);  // <-- 1. callback is a temp, so what does std::move do here?
    }
    void process_data(std::string data)
    {
        if (!m_callback(data)) { /* do error handling */ }
    }
};

int main() {
    printer p;
    worker w;

    w.set_callback( std::move([&](std::string s){ return p.print(s); }) ); // <-- 2. what does std::move do here?
    w.process_data("hello world2");
}

Note: I have std:: move() called twice... now this works (surprisingly to me) but I have both only to show what I am trying. My questions are:

  1. Should I use std::move() in the set_callback() function to "pull" out the temp, and if I use this is there really a copy or does std:: move() mean that its not really a copy?
  2. Should I use std:: move() to pass in the lambda... and is that even correct.
  3. I guess I don't understand why this code works with two std:: moves()... which implies that I still don't understand what std:: move() is doing - so if someone can elighten me on what is going on here that would be great!
  4. I know that I can pass by value, but my goal is to move the temp so that I don't have a copy of it. Is that what is meant by perfect forwarding?

My example can be seen here in wandbox: https://wandbox.org/permlink/rJDudtg602Ybhnzi

UPDATE The reason for me trying to use std::move was to avoid copying the lambda around. (I think that is called forwarding/perfect-forwarding)...but I think I am making a hash of it!

Geezer
  • 5,600
  • 18
  • 31
code_fodder
  • 15,263
  • 17
  • 90
  • 167

3 Answers3

7

In this line,

w.set_callback( std::move([&](std::string s){ return p.print(s); }) );

you cast an rvalue to an rvalue. This is a no-op and hence pointless. Passing a temporary to a function that accepts its parameter by value is fine by default. The function argument is likely to be instantiated in place anyhow. In the worst case, it is move-constructed, which doesn't require an explicit call to std::move on the function argument - again, as it is already an rvalue in your example. To clarify the situation, consider this different scenario::

std::function<bool(std::string)> lValueFct = [&](std::string s){ /* ... */ }

// Now it makes sense to cast the function to an rvalue (don't use it afterwards!)
w.set_callback(std::move(lValueFct));

Now for the other case. In this snippet

void set_callback(callback_fn callback)
{
    m_callback = std::move(callback);
}

you move-assign to m_callback. This is fine, as the parameter is passed by value and not used afterwards. One good resource on this technique is Item 41 in Eff. Modern C++. Here, Meyers also points out, however, that while it's generally fine to employ pass-by-value-then-move-construct for initialization, it's not necessarily the best option for assignment, because the by-value parameter must allocate internal memory to hold the new state, while that could use an existing buffer when directly copied from a const-qualified reference function parameter. This is exemplified for std::string arguments, and I'm not sure how this can be transferred to std::function instances, but as they erase the underlying type, I could imagine this being an issue, especially for larger closures.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
lubgr
  • 37,368
  • 3
  • 66
  • 117
  • You're right about copy sometimes being better than move for assignments, but your reasoning is not. The reason is that *the by-value parameter* **must** create a buffer for the copy of the contents (which will then be moved to the LHS of the assignment), where the contents of the `const &` would have to be copied, but the LHS's buffer could possibly be reused. IOW, the potentially unnecessary allocation happens in the parameter, not in the assignment's LHS. – Angew is no longer proud of SO Sep 13 '18 at 07:44
  • @Angew Thanks for the hint, that's actually what I was trying to say, let me clarify. – lubgr Sep 13 '18 at 07:46
  • 1
    @lubgr that's a nice description, thanks. Just one question in your comment `// Now it makes sense to cast the function to an lvalue (don't use it afterwards!)` do you mean `rvalue ref` instead of `lvalue` or did I miss-understand? – code_fodder Sep 13 '18 at 07:50
  • YES!! That's quite a dumb type. Thanks for spotting it! – lubgr Sep 13 '18 at 07:51
5

I guess I don't understand why this code works with two std::moves... which implies that I still don't understand what std::move is doing

std::move is there to make it explicit in cases you are intending to move from an object. Move semantics are designed to work with rvalues. Thus, std::move() takes any expression (such as an lvalue) and makes an rvalue out of it. This usage arises typically when you need to allow an lvalue to be passed to the function overloads that accept rvalue reference as an argument, such as move constructors and move assignment operators. The idea of moving is to efficiently be transferring resources instead of making copies.

In your snippet you're not using std::move() in an invalid way, hence this code works. In the rest of the answer we try to see whether this usage is advantageous or not.

Should I use std::move to pass in the lambda

Seemingly no, you have no reason of doing that in the snippet. First of all, you are calling move() on a what is already an rvalue. Further, syntactically, set_callback() is receiving its std::function<bool(std::string)> argument by value, of which your lambda is initializing an instance just fine at present.

Should I use std::move in the set_callback() function

It isn't 100% clear what you are gaining by using the move version of the assignment operator onto the m_callback member variable, instead of the regular assignment. It won't cause any undefined behavior though, as you are not trying to use the argument after moving it. Also, since C++11 the callback parameter in set_callback() will be move constructed for rvalues such as your temporary, and copy constructed for an lvalue, such as if you'd call it like this:

auto func = [&](std::string s){ return p.print(s); };
w.set_callback(func);

What you need to be considering is whether inside the method, moving is better then copying in your case. Moving involves its own implementation of the move assignment for the relevant type. I'm not just saying QOI here, but consider that when moving you need to release whatever resource m_callback was holding up to that point, and for the scenario of moving from a constructed instance (as we've covered that callback has been either copy constructed or move-constructed from its argument), that's adding to the cost this construction already had. Not sure that such a moving overhead applies in your case, but still your lambda is a not obviously expensive to copy as it is. That said, opting for two overloads, one taking a const callback_fn& callback and copy-assigning inside and one taking a callback_fn&& callback and move-assigning inside would allow to mitigate this potential issue altogether. As in either one you don't construct anything for the parameter and overall you're not necessarily releasing old resources as an overhead, as when performing the copy-assignment, one can potentially use the already existing resources of the LHS by copying onto them instead of releasing it prior to moving the ones from the RHS.

I know that I can pass by value, but my goal was to move the temp so that I don't have a copy of it (perfect forwarding?)

In the context of type deduction (template or auto), a T&& is a forwarding reference, not an rvalue reference. As such you only have to write the function once (template function, no overloads), and relying internally on std::forward (equivalent to static_cast<T&&>) will make sure that in any use-case, the above described path for using the two overloads is preserved in terms of the cost being a copy-assignment for an lvalue call and a move-assignment for an rvalue call:

template<class T>
void set_callback(T&& callback)
{
    m_callback = std::forward<T>(callback);
}
Geezer
  • 5,600
  • 18
  • 31
  • @SkepticalEmpiricist I know that I can pass by value, but my goal was to move the temp so that I don't have a copy of it (perfect forwarding?).. I did not explain that clearly. – code_fodder Sep 13 '18 at 07:35
  • I just feel the first point isn't fleshed out enough for me to make up my mind. I do think there is room to discuss the pro's and con's of moving vs copying here, like lubgr does. To me it seems like your third statement contradicts the first. `callback` *is and lvalue*, so makeing an rvalue out of it and stealing its insides seems to have merit on the face of it anyway. – StoryTeller - Unslander Monica Sep 13 '18 at 07:49
  • @SkepticalEmpiricist I would be much obliged if you could tap out somthing about how perfect forwarding should be done... I think I have just got it plain wrong here by the sound of it : ) – code_fodder Sep 13 '18 at 07:55
  • @SkepticalEmpiricist thanks for taking the trouble to expalin that for me :) . I am thinking (from what you said) that I could just make the set_callback take a `auto &&fn` as the parameter (to save me going full-template) because (as you mentioned) both use type deduction... does that work? (selected as answer due to the time spent explaining : ) – code_fodder Sep 13 '18 at 10:51
  • 1
    @SkepticalEmpiricist yes, but I meant also that you spent the time explaining the "extra" information that was not strictly part of the original question : ) – code_fodder Sep 13 '18 at 12:37
  • @code_fodder That's completely OK. If it's fine by you then I'll add the "extra" question to the original post of yours, so it could be visible upon first look. – Geezer Sep 13 '18 at 12:39
  • @SkepticalEmpiricist actually that is a good idea... I probably just phrased my question poorly so please do : ) – code_fodder Sep 13 '18 at 12:42
1

According to c++ reference std::move is just a cast to rvalue reference.

  1. You don't need to use std::move inside your set_callback method. std::function is CopyConstructible and CopyAssignable (docs), so you can just write m_callback = callback;. If you use std::function move constructor, the object you moved from will be "unspecified" (but still valid), in particular it might be empty (which doesn't matter because it is a temporary). You can also read this topic.
  2. Same thing here. Lambda expression is a temporary and expires after the call to set_callback, so it doesn't matter if you move it or copy it.
  3. The code works, because your callbacks are constructed (move constructed) correctly. There is no reason for the code not to work.

Below is the example with the situation when std::move makes a difference:

callback_fn f1 = [](std::string s) {std::cout << s << std::endl; return true; };
callback_fn f2 = f1;
f1("xxx");  // OK, invokes previously assigned lambda
f2("xxx");  // OK, invokes the same as f1
f2 = std::move(f1);
f1("xxx");  // exception, f1 is unspecified after move

Output:

xxx
xxx
C++ exception with description "bad_function_call" thrown in the test body.
pptaszni
  • 5,591
  • 5
  • 27
  • 43
  • 1
    Your point 1 sounds flawed. `std::vector` is also CopyAssignable, but there's a huge difference between copying and moving it. Even `std::function` is likely to contain dynamically allocated memory, so moving it instead of copying can save on an (expensive!) allocation. – Angew is no longer proud of SO Sep 13 '18 at 07:47
  • @Angew, yes you are right, copying might be more costly than moving (or might not be), however I really doubt that you can easily spot the difference in the application. I investigated the assembly code produced [here](https://godbolt.org/z/n8bGTA), the difference is that move constructor is using `swap` (but it might be implementation dependent). – pptaszni Sep 13 '18 at 08:09
  • 2
    @Ptaq666 It might not matter in this case, but the OP is obviously trying to grasp the general principles. Make the callable big enough to not fit into `std::functions`'s small-buffer optimisation, and then compare the assembly. – Angew is no longer proud of SO Sep 13 '18 at 08:12