std::algorithm 多次调用函数 lambda 捕获

std::algorithm functions lambda capture called several times

据我所知,lambda 捕获变量的生命周期绑定到 lamda 对象的生命周期。例如,在这种情况下:

#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());
}

输出为:

dtor

即使我们多次调用 lamda,“dtor”也只放置了一次,这是预期的。

但是 std::algorithm 函数有些奇怪。如果我们使用它们:

#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());
}

输出如下:

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

std::erase_if 上有 7 个“dtor”,std::none_of 上有 6 个“dtor”,std::any_of 上有 7 个“dtor”,正常调用 lambda 时只有 1 个“dtor”(如预期的)。这些数字与容器大小无关。我试过并得到了相同的数字。

所以,问题是,它是错误还是取决于 std::algorithm 函数的实现细节?看起来,这些 std::algorithm 函数可能会多次构造和销毁 lamda 对象,这就是我们的 lambda 捕获变量被多次构造和销毁的原因。

顺便说一下,另一个奇怪的事情是 MSVC 构建上的这些数字(2)低于 GCC 和 Clang 但仍然大于 1。这是 MSVC 输出:

dtor
dtor
continue
dtor
continue2
dtor
dtor
heyyyyyyyyyyyyy
dtor
dtor

这里可以测试:https://godbolt.org/z/nWd77c9o6

现在,我决定不在调用 std::algorithm 函数时创建 lambda 捕获变量(如果它们不是基本类型),我将创建变量并在 lambda 捕获中通过引用传递。

我无法重现确切的输出,但我可以重现 lambda 捕获中的对象被多次复制和销毁。这是由于标准算法的设计及其留给库实现者的自由。

特别是,传递给标准算法的可调用对象是按值传递的,因此复制它们的成本应该很低(否则,可以将它们包装在某种引用包装器中)。当将这样的对象(如您的情况下的 lambda)传递给算法时,您必须期望它被传递给其他算法。由于许多标准算法都是可重用的构建块,因此一种算法通常根据一种或多种其他算法来实现。当一个可调用对象被传递给这些其他算法时,它被复制 - 因此你的输出。

为了完整起见,这是我可以观察到的输出:

dtor
dtor
dtor
dtor
dtor
dtor
dtor
continue

只需在 lambda 表达式的闭包中通过引用捕获 SomeCla 的一个实例,即:

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

到处改这个给了我:

continue
continue2
heyyyyyyyyyyyyy
dtor