Home > OS >  std::algorithm functions lambda capture called several times
std::algorithm functions lambda capture called several times

Time:03-03

As far as I know, lambda capture variable lifecycle is bound to lifecycle of lamda object. For example, in this case:

#include <string>
#include <vector>

using namespace std;

class SomeCla {
public:
    constexpr SomeCla(int i, float f) noexcept : _i(i), _f(f) {}

    ~SomeCla() {
        puts("dtor");
    }

    SomeCla(const SomeCla&) = default;
    SomeCla(SomeCla&&) = default;

    SomeCla& operator=(const SomeCla&) = default;
    SomeCla& operator=(SomeCla&&) = default;

    constexpr float total() const noexcept { return static_cast<float>(_i)   _f; }

private:
    int _i;
    float _f;
};

int main() {
    vector<float> vec = { 1.0f };

    const auto filler = [someCla = SomeCla(1, 2.0f)](vector<float>& someVec, int val) {
        someVec.push_back(someCla.total()   static_cast<float>(val));
    };

    for (int i = 0; i < 10;   i) {
        filler(vec, i * 3);
    }

    return static_cast<int>(vec.size());
}

Output is:

dtor

"dtor" was put only once even if we're calling lamda several times, which is expected.

But there are strange things are about std::algorithm functions. If we use them:

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class SomeCla {
public:
    constexpr SomeCla(int i, float f) noexcept : _i(i), _f(f) {}

    ~SomeCla() {
        puts("dtor");
    }

    SomeCla(const SomeCla&) = default;
    SomeCla(SomeCla&&) = default;

    SomeCla& operator=(const SomeCla&) = default;
    SomeCla& operator=(SomeCla&&) = default;

    constexpr float total() const noexcept { return static_cast<float>(_i)   _f; }

private:
    int _i;
    float _f;
};

int main() {
    vector<float> vec = { 1.0f };
    erase_if(vec, [someCla = SomeCla(1, 2.0f)](float ele) {
        return ele == someCla.total();
    });

    puts("continue");

    {
        const auto filler = [someCla = SomeCla(1, 2.0f)](vector<float>& someVec, int val) {
            someVec.push_back(someCla.total()   static_cast<float>(val));
        };

        for (int i = 0; i < 10;   i) {
            filler(vec, i * 3);
        }
    }

    puts("continue2");

    ignore = none_of(vec.cbegin(), vec.cend(), [someCla = SomeCla(1, 2.0f)](float ele) { return ele == -1.0f; });

    puts("heyyyyyyyyyyyyy");

    ignore = any_of(vec.cbegin(), vec.cend(), [someCla = SomeCla(1, 2.0f)](float ele) { return ele == 1.0f; });

    return static_cast<int>(vec.size());
}

Output will be like:

dtor
dtor
dtor
dtor
dtor
dtor
dtor
continue
dtor
continue2
dtor
dtor
dtor
dtor
dtor
dtor
heyyyyyyyyyyyyy
dtor
dtor
dtor
dtor
dtor
dtor
dtor

7 "dtor" on std::erase_if, 6 "dtor" on std::none_of, 7 "dtor" on std::any_of and only 1 "dtor" on normal call of lambda (as expected). And these numbers are independent from container size. I tried and got same numbers.

So, the question is, is it a bug or dependent on implementation details of std::algorithm functions? It seems, these std::algorithm functions probably constructs and destructs lamda object several times, that's the why our lambda capture variable was constructed and destructed several times.

By the way, another strange thing is these numbers (which is 2) on MSVC build are lower than GCC and Clang but still more than 1. Here is MSVC output:

dtor
dtor
continue
dtor
continue2
dtor
dtor
heyyyyyyyyyyyyy
dtor
dtor

Here it can be tested: https://godbolt.org/z/nWd77c9o6

For now, I decided not to create lambda capture variables (if they are not basic types) on calling std::algorithm functions, I'll create the variable and pass by reference on lambda capture instead.

CodePudding user response:

I cannot reproduce the exact output, but I can reproduce that the object in the lambda capture is copied and destroyed multiple times. This is due to the design of standard algorithm and the freedom it leaves to library implementers.

In particular, callable objects passed to standard algorithms are passed by value, so they are expected to be cheap to copy (otherwise, they can be wrapped in a reference wrapper of some kind). And when passing such an object (like the lambda in your case) to an algorithm, you must expect that it is passed on to other algorithms. Since many of the standard algorithm are reusable building blocks, one algorithm is often implemented in terms of one or more other algorithms. When a callable object is passed to these other algorithm, it is copied - hence your output.

For completeness, this is the output I could observe:

dtor
dtor
dtor
dtor
dtor
dtor
dtor
continue

CodePudding user response:

Just capture an instance of SomeCla by reference in the closure of your lambdas, ie:

SomeCla someCla(1,2.0f);
erase_if(vec, [&someCla](float ele) {
    return ele == someCla.total();
});

Changing this everywhere gave me:

continue
continue2
heyyyyyyyyyyyyy
dtor
  • Related