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

std::algorithm functions lambda capture called several times

提问人:Oğuzhan Türk 提问时间:3/2/2022 最后编辑:lubgrOğuzhan Türk 更新时间:3/2/2022 访问量:75

问:

据我所知,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

“dtor”只放了一次,即使我们多次打电话给 Lamda,这是意料之中的。

但是关于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 std c++20 捕获

评论

1赞 lubgr 3/2/2022
我将 C++11 标签变成了 C++20,因为只有 C++20。erase_if

答:

2赞 lubgr 3/2/2022 #1

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

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

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

dtor
dtor
dtor
dtor
dtor
dtor
dtor
continue

评论

0赞 Oğuzhan Türk 3/2/2022
谢谢你的解释。在将 lambda 传递到 std::algorithm 函数中时,仅当变量为基本类型时,我才会创建 lambda 捕获变量。
1赞 Jean-Baptiste Yunès 3/2/2022 #2

只需在 lambda 的闭包中捕获 by reference 的实例,即:SomeCla

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

到处改变这一点给了我:

continue
continue2
heyyyyyyyyyyyyy
dtor

评论

1赞 Oğuzhan Türk 3/2/2022
谢谢你的例子。在将 lambda 传递到 std::algorithm 函数中时,仅当变量为基本类型时,我才会创建 lambda 捕获变量。因此,对于这种情况,我将按照您的建议或像这样使用,因为它是基本类型。erase_if(vec, [floVal = SomeCla(1, 2.0f).total()](float ele) { return ele == floVal; });