MVittiS
MVittiS

Reputation: 434

Confusion about C++ functor/lambda argument-passing in STL algorithms

I like C++11 and its ability to combine STL algorithms to lambdas; it makes the STL much more approachable and useful to everybody. But one thing that I don't understand is what happens inside an STL algorithm (like std::accumulate) regarding object copying or referencing inside the lambda (or wherever functor you give to it).

My three questions are:

  1. Are there any guidelines regarding wherever I should care about pass-by-reference vs. pass-by-value in lambdas/functors?
  2. Does it matter at all if you declare a lambda inside an algorithm that takes its arguments by reference ([](Type &a, Type &b){}), and will it be more optimal than a regular variant; or is it just syntax sugar, the compiler will optimize it anyway, and I could simply omit the ampersands?
  3. Does the C++ Standard has any provision about this?

As for question #2, a quick experiment in Godbolt's GCC page (using compilation flags -stc=c++11 -Os) seems to suggest the latter, as the generated assembly from the code below is identical wherever I use [](T i1, T i2) or [](T &i1, T &i2); I don't know if those results could be generalized to more complex types/objects, however.

Example #1:

#include<array>
#include<numeric>

template <typename T>
T vecSum(std::array<T, 4> &a){
  return std::accumulate(a.begin(), a.end(), T(0),
    [](T i1, T i2) {
        return std::abs(i1) + std::abs(i2);
    }
  );
}

void results() {
    std::array<int, 4> a = {1,-2, 3,-4};
    std::array<int, 4> b = {1,-2,-3, 4};

    volatile int c = vecSum(a) + vecSum(b);
}

Example #2:

#include<string>
#include<array>
#include<numeric>

struct FatObject {
    std::array<int, 1024*1024> garbage;
    std::string string;

    FatObject(const std::string &str) : string(str) {
        std::fill(garbage.begin(),garbage.end(),0xCAFEDEAD);
    }

    std::string operator+(const FatObject &rhs) const {
        return string + rhs.string;
    }
};

template <typename T>
T vecSum(std::array<T, 4> &a){
  return std::accumulate(a.begin(),a.end(),T(0),
    [](T i1, T i2) {
        return i1 + i2;
    }
  );
}

void results() {
    std::array<FatObject, 4> a = { 
        FatObject("The "),
        FatObject("quick "),
        FatObject("brown "),
        FatObject("fox")
    };
    std::array<FatObject, 4> b = {
        FatObject("jumps "),
        FatObject("over "),
        FatObject("the "),
        FatObject("dog ")
    };

    volatile std::string c = vecSum(a) + vecSum(b);
}

Upvotes: 1

Views: 1305

Answers (2)

Emerald Weapon
Emerald Weapon

Reputation: 2540

Your question is quite broad, but here is my concise answer.

1) In general, the guidelines for passing by-value vs by-reference in lambdas or functors are the same as they are for any regular function or method (a lambda is a functor created on the fly for you, which is a an object with an operator()(T)). The choice is mostly specific to your case, for example if the lambda/functor needs read-only access to its arguments you tipycally would pass a const reference.

2) Inside an algorithm that accepts a callable object as an argument (and as a template parameter) the compiler is bound to respect the rules of the language. Therefore parameters will be passed by value or by reference internally as per the signature of the lambda/functor.

Keep in mind that copy elision may enter into play, but that is a separate issue, not directly related to the fact that you are calling a lambda inside an standard library algorithm.

The example with int is too simple. I suggest you to experiment with actual objects.

3) C++ Standard provides precise definitions for the conditions where copy elision occurs, as well as the requirements on the signature of a lambda/functor parameter for a particular Standard Library algorithm.

However, it will not be easy in general to know if the internal implementation of a particular algorithm is going to call the lambda/functor in a way that meets copy elision conditions.

Note that the requirements on signature have some degree of flexibility, for example in the std::accumulate documentation we have

The signature of the function should be equivalent to the following: Ret fun(const Type1 &a, const Type2 &b); The signature does not need to have const &.

so you can choose to pass by value or by reference as you see fit.

Upvotes: 2

Łukasz Seremak
Łukasz Seremak

Reputation: 19

In lambdas/functions are the same rules as in all C++.

You should use non-const reference if the intent of the function is to modify the object for the caller. The function should use const& if it is just using the object without changing it. And it should pass by value if it is going to copy/move the object into its internal storage.

If you pass a small object like int it makes no difference if you pass by value or by reference. When you start to pass a big object it makes a big impact on performance.

Upvotes: 0

Related Questions