Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

std::algorithm functions lambda capture called several times

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading