Should I Pass an Std::Function by Const-Reference

Should I copy an std::function or can I always take a reference to it?

Can I store the function as a reference since std::function is just a function-pointer and the 'executable code' of the function is guaranteed to stay in memory?

std::function is very much not just a function pointer. It's a wrapper around an arbitrary callable object, and manages the memory used to store that object. As with any other type, it's safe to store a reference only if you have some other way to guarantee that the referred object is still valid whenever that reference is used.

Unless you have a good reason for storing a reference, and a way to guarantee that it remains valid, store it by value.

Passing by const reference to the constructor is safe, and probably more efficient than passing a value. Passing by non-const reference is a bad idea, since it prevents you from passing a temporary, so the user can't directly pass a lambda, the result of bind, or any other callable object except std::function<int(int)> itself.

std::function - value vs. reference argument

cppreference says that std::function<R(Args...)>::operator() has signature

R operator()(Args... args) const;

and that it calls the stored callable f basically by f(std::forward<Args>(args)...). The performance characteristics depend on both the template argument and the lambda's argument type and I think it would be helpful to just see everything that can happen. In your case, you have 2 std::function types, 2 callables, and 3 possible value categories for the argument, giving you 12 possibilities.

  • std::function<void(VeryBigType)> f = [](VeryBigType i) { }

    • If you call this with an lvalue, like

      VeryBigType v;
      f(v);

      This will copy v into the argument of operator(), and then operator() will pass an rvalue to the lambda, which will move the value into i. Total cost: 1 copy + 1 move

    • If you call this with a prvalue, like

      f(VeryBigType{});

      Then this will materialize the prvalue into the argument of operator(), then pass an rvalue to the lambda, which will move it into i. Total cost: 1 move

    • If you call this with an xvalue, like

      VeryBigType v;
      f(std::move(v));

      This will move v into the argument of operator(), which will pass an rvalue to the lambda, which will move it again into i. Total cost: 2 moves.

  • std::function<void(VeryBigType)> f = [](VeryBigType const &i) { }

    • If you call this with an lvalue, this will copy once into the argument of operator(), and then the lambda will be given a reference to that argument. Total cost: 1 copy.

    • If you call this with a prvalue, this will materialize it into the argument of operator(), which will pass a reference to that argument to the lambda. Total cost: nothing.

    • If you call this with an xvalue, this will move it into the argument of operator(), which will pass a reference to that argument to the lambda. Total cost: 1 move.

  • std::function<void(VeryBigType const&)> f = [](VeryBigType i) { }

    • If you call this with an lvalue or xvalue (i.e. with a glvalue), operator() will receive a reference to it. If you call this with a prvalue, it will be materialized into a temporary, and operator() will receive a reference to that. In any case, the inner call to the lambda will always copy. Total cost: 1 copy.
  • std::function<void(VeryBigType const&)> f = [](VeryBigType const &i) { }

    • Again, no matter what you call this with, operator() will receive just a reference to it, and the lambda will just receive the same reference. Total cost: nothing.

So, what did we learn? If both the std::function and the lambda take references, you avoid any extraneous copies and moves. Use this when possible. Putting a by-value lambda inside a by-const-lvalue-reference std::function, however, is a bad idea (unless you have to). Essentially, the lvalue reference "forgets" the value category of the argument, and the argument to the lambda is always copied. Putting a by-const-lvalue-reference lambda inside a by-value std::function is pretty good performance-wise, but you only need to do so if you're calling into other code that expects a by-value std::function, because otherwise a by-reference std::function achieves the same thing but with less copying and moving. Putting a by-value lambda inside a by-value std::function is slightly worse than putting a by-const-lvalue-reference lambda inside of it, due to an extra move in all calls. It would be better to instead take the argument of the lambda by-rvalue-reference, which is pretty much the same as taking it by-const-lvalue-reference except you still can mutate the argument, just as if you took it by value anyway.

TL;DR: By-value and rvalue-reference arguments in a std::function template argument should correspond to by-rvalue-reference or by-const-lvalue-reference arguments in the lambda you put inside the std::function. By-lvalue-reference arguments in the type should correspond to by-lvalue-reference arguments in the lambda. Anything else incurs additional copies or moves, and should only be used when needed.

When is a const reference better than pass-by-value in C++11?

The general rule of thumb for passing by value is when you would end up making a copy anyway. That is to say that rather than doing this:

void f(const std::vector<int>& x) {
std::vector<int> y(x);
// stuff
}

where you first pass a const-ref and then copy it, you should do this instead:

void f(std::vector<int> x) {
// work with x instead
}

This has been partially true in C++03, and has become more useful with move semantics, as the copy may be replaced by a move in the pass-by-val case when the function is called with an rvalue.

Otherwise, when all you want to do is read the data, passing by const reference is still the preferred, efficient way.

C++ std::function parameters const reference

Note that IN, i.e. the type decltype(*std::declval<T>().begin()) is a reference int&; then for const IN&, const is qualified on the reference and just gets ignored. So given IN is int&, const IN& -> int& & -> int&.

See the behavior of decltype:

  1. ...
  2. If the argument is any other expression of type T, and

    a) ...

    b) if the value category of expression is lvalue, then decltype yields T&;

    c) ...

You can remove the reference part by std::remove_reference or std::decay. e.g.

template<typename T,
typename IN = std::remove_reference_t<decltype(*std::declval<T>().begin())>,
// ^^^^^^^^^^^^^^^^^^^^^^^^ ^
typename FT = function<ostream&(ostream&, const IN&)>
>
void func(ostream& ss, const T& v,
FT op = [](ostream& os, const IN&v)->ostream&{ return os << v; }) {
for (const auto& i: v) {
std::invoke(op, ss, i);
}
}

Should I pass std::function by-value or by (rvalue)-reference?

First, std::function is required to avoid allocation in the case where the target is a function pointer of std::reference_wrapper; implementations are encouraged to avoid allocation for "small" targets:

[...] for example, where f's target is an object holding only a pointer or reference to an object and a member function pointer.

That means that copying a std::function whose target is large will involve an allocation and a copy of the target (reference counting is not permitted, since the target might have mutable state). However, in your specific case the copy will be elided since you are calling your function with a prvalue temporary.

In your specific case, since you are calling the function immediately you don't need to worry about taking ownership of it, so it would be better to take the function by const reference. Taking it by rvalue reference would cause a headache to a user who has an existing lvalue or const reference to a function; they would need to std::move it (in the former case) or create a prvalue temporary copy (in the latter case).

Does passing fundamental values by const reference really hurt performance?

On platforms where the fundamental type in question fits into a register, a decent compiler should eliminate const references from parameters if it can see both sides of the call. For templates that is usually a given (unless they were explicitly instantiated somewhere). Since your library presumably has to be templated all the way down, this will apply to your case.

It's possible that your end users will have bad compilers or platforms where e.g. a double does not fit into a register. I don't see why you'd be incentivized to make micro-optimizations for these particular users, but maybe you do.

It's also possible that you want to explicitly instantiate all templates in your library for some set of types and provide implementation-less header files. In that case the user's compiler must obey whatever calling conventions exist on that platform and will probably pass fundamental types by reference.

Ultimately, the answer is "profile the relevant and representative use cases" if you don't have faith in the compiler(s).


Edit (removed macro solution): As suggested by Jarod42, the C++ way would be using an alias template. This also avoids the lack of deduction that the asker was running into with their original approach:

template<class T>
using CONSTREF = const T&; // Or just T for benchmarking.

https://godbolt.org/z/mopZ6B

As cppreference says:

Alias templates are never deduced by template argument deduction when deducing a template template parameter.

Are the days of passing const std::string & as a parameter over?

The reason Herb said what he said is because of cases like this.

Let's say I have function A which calls function B, which calls function C. And A passes a string through B and into C. A does not know or care about C; all A knows about is B. That is, C is an implementation detail of B.

Let's say that A is defined as follows:

void A()
{
B("value");
}

If B and C take the string by const&, then it looks something like this:

void B(const std::string &str)
{
C(str);
}

void C(const std::string &str)
{
//Do something with `str`. Does not store it.
}

All well and good. You're just passing pointers around, no copying, no moving, everyone's happy. C takes a const& because it doesn't store the string. It simply uses it.

Now, I want to make one simple change: C needs to store the string somewhere.

void C(const std::string &str)
{
//Do something with `str`.
m_str = str;
}

Hello, copy constructor and potential memory allocation (ignore the Short String Optimization (SSO)). C++11's move semantics are supposed to make it possible to remove needless copy-constructing, right? And A passes a temporary; there's no reason why C should have to copy the data. It should just abscond with what was given to it.

Except it can't. Because it takes a const&.

If I change C to take its parameter by value, that just causes B to do the copy into that parameter; I gain nothing.

So if I had just passed str by value through all of the functions, relying on std::move to shuffle the data around, we wouldn't have this problem. If someone wants to hold on to it, they can. If they don't, oh well.

Is it more expensive? Yes; moving into a value is more expensive than using references. Is it less expensive than the copy? Not for small strings with SSO. Is it worth doing?

It depends on your use case. How much do you hate memory allocations?



Related Topics



Leave a reply



Submit