0

I have an API which just enters a subscription into a vector of callbacks. The vector uses std::function which could be partially heap allocated, hence move operations on std::function make sense. Now I could implement this API in two ways:

  • Take by value
  • Take by rvalue reference

The chances that the user just puts in a lambda (rvalue ref) are really big in this case. If I take by value it will move-construct the new function object in my function body, but I actually just want to forward it to the vector element construct. This would therefore be an unnecessary inefficiency.

On the other hand, if I take by rvalue ref, the user is forced to provide an rvalue. But what if he wants to distribute the function object to multiple sinks? Right, he just has to make the copy the object himself before the call.

Now: Why not always aim for the second design, which is, taking rvalue references and let the caller make the copy? It doesn't imply any penalties on the user, he just hast to remember to copy the object first IF he wants to move the object elsewhere as well, but other than that the user stays in full controll of how the API should behave and will never have the performance penalty associated with move-constructing the object.

Demo

#include <functional>
#include <cstdio>
#include <vector>


struct entity
{
    auto on_change(std::function<void()>&& cb)
    {
        callbacks_.push_back(std::move(cb));
    }

    auto print_all() {
        std::for_each(callbacks_.cbegin(), callbacks_.cend(), [](const auto cb){ cb(); });
    }

    std::vector<std::function<void()>> callbacks_;
};

auto foo() -> void {
    printf("Hello from function!\n");
}

int main()
{
    entity e,d;

    // Normal case
    e.on_change([](){ printf("Hello from lambda!\n"); });

    // Special case
    auto fn = std::function<void()>([]{ printf("Hello from function that gets distributed multiple times!\n"); });
    // these two don't work
    // e.on_change(fn); 
    // d.on_change(fn);
    // But why not like this?
    e.on_change(std::function<void()>(fn)); 
    d.on_change(std::move(fn));

    e.print_all();
    d.print_all();
}

Output:

Hello from lambda!
Hello from function that gets distributed multiple times!
Hello from function that gets distributed multiple times!

Note: This other question deals with the differentiation of rvalue vs lvalue which is not what this question is about.

glades
  • 3,778
  • 1
  • 12
  • 34
  • Why not what? It's your API. Design it as you see fit. I personally also love to provide "move"-only functions, inspired by ownership paradigm of Rust language. I agree that it allows better data flow at a cost occasional boilerplate for the API user – Alexey S. Larionov Feb 15 '23 at 08:49
  • Besides, `std::vector` has `emplace_back` rather than `push_back` – Alexey S. Larionov Feb 15 '23 at 08:50
  • @AlexeyLarionov Is this the new way to go? Personally I also see almost no downsides. push_back has an && overload and will forward to emplace_back internally anyway, so no worries :) – glades Feb 15 '23 at 08:55
  • I don't undrestand why you think there are "ways" to go. If I want to retain the object ownership for the caller method, I would make a value/reference argument. If I'm OK with never using the given object again on the caller side, I would make an rvalue ref argument, and additionally add a block `{ }` on the caller side if I transfer ownership not from a temporary object, but from an object bound to a variable (in order to shorten the scope of the moved variable) – Alexey S. Larionov Feb 15 '23 at 09:03
  • Did you notice that `e.on_change(fn);` isn't possible with only rvalue-reference arg? So it's inconvenient for the user of the API. For this reason it's common to take callbacks in by a forwarding reference (templated &&) instead. – rustyx Feb 15 '23 at 09:04
  • @rustyx IMHO I adore explicit moving like so: `{ auto fn = ...; e.on_change(std::move(fn)) }` – Alexey S. Larionov Feb 15 '23 at 09:06
  • @AlexeyLarionov Why so complicated? The temporary lifetime is extended until the end of the full expression only, so you can just write the temporary expression inside the on_change paranthesis. – glades Feb 15 '23 at 10:02
  • @rustyx Right that's another possibility. That's basically saying "I just need an object, hand me ownership or don't I don't care" – glades Feb 15 '23 at 10:05
  • @glades I don't like cumbersome statements, that's why. Actually you can go without `std::move` by making `fn` an rvalue ref by definition, I missed that – Alexey S. Larionov Feb 15 '23 at 10:10
  • @AlexeyLarionov What do you mean? If you assign a name to an rvalue, you always need to std::move(). The value category doesn't matter but the "expression rvalue-ness" – glades Feb 15 '23 at 10:12
  • @glades Yeah, my bad, I just checked and it behaves like an lvalue requiring to move anyway – Alexey S. Larionov Feb 15 '23 at 10:27

0 Answers0