为什么捕获无状态 lambda 有时会导致大小增加?

Why does capturing stateless lambdas sometimes results in increased size?

给定一个 lambda 链,其中每个 lambda 都按值捕获前一个:

auto l1 = [](int a, int b) { std::cout << a << ' ' << b << '\n'; };
auto l2 = [=](int a, int b) { std::cout << a << '-' << b << '\n'; l1(a, b); };
auto l3 = [=](int a, int b) { std::cout << a << '#' << b << '\n'; l2(a, b); };
auto l4 = [=](int a, int b) { std::cout << a << '%' << b << '\n'; l3(a, b); };

std::cout << sizeof(l4);

我们可以观察到,结果 sizeof of l4 is equal to 1.

这对我来说很有意义。我们按值捕获 lambda,每个对象都必须 sizeof 等于 1,但由于它们是无状态的,因此适用类似于 [[no_unique_address]] 的优化(特别是因为它们都有独特的类型)。

但是,当我尝试创建用于链接比较器的通用构建器时,this optimization not longer takes place:

template <typename Comparator>
auto comparing_by(Comparator&& comparator) {
    return comparator;
}

template <typename Comparator, typename... Comparators>
auto comparing_by(Comparator&& comparator, Comparators&&... remaining_comparators) {
    return [=](auto left, auto right) {
        auto const less = comparator(left, right);
        auto const greater = comparator(right, left);
        if (!less && !greater) {
            return comparing_by(remaining_comparators...)(left, right);
        }
        return less;
    };
}

struct triple {
    int x, y, z;
};

auto main() -> int {
    auto by_x = [](triple left, triple right) { return left.x < right.x; };
    auto by_y = [](triple left, triple right) { return left.y < right.y; };
    auto by_z = [](triple left, triple right) { return left.z < right.z; };

    auto comparator = comparing_by(by_x, by_z, by_y);

    std::cout << sizeof(comparator);
}

注意 1:我知道 comparing_by 效率低下,有时会以冗余方式调用比较器。

为什么在上述情况下 comparator 的结果 sizeof 等于 3 而不是 1?毕竟,它仍然是无状态的。我哪里错了?或者这只是三大编译器中的一个优化遗漏?

注2:这纯属学术问题。我不是要解决任何特定问题。

第一个例子中发生的事情并不是你想的那样。假设 l1 的类型为 L1l2 L2 等。这些是这些类型的成员:

struct L1 {
   // empty;
};

sizeof(L1) == 1

struct L2 {
    L1 l1;
};

sizeof(L2) == sizeof(L1)  // 1

struct L3 {
    L2 l2;
};

sizeof(L3) == sizeof(L2)  // 1

struct L4 {
    L3 l3;
};

sizeof(L4) == sizeof(L3)  // 1

在下一个示例中,您按值捕获所有 lambda,因此闭包类型具有三个 non-overlapping 成员,因此大小至少为 3。

[[no_unique_address]] 不能普遍应用于闭包类型的数据成员(考虑将其地址放在全局映射中的空 class)。

编译器可以对“行为良好的类型”使用空基优化(可能是 trivilly-copyable 空类型?),因此这可能是一个错过的优化。该标准说明了可以做什么 ([expr.prim.lambda.closure]p2):

The closure type is not an aggregate type. An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:

  • the size and/or alignment of the closure type,
  • whether the closure type is trivially copyable ([class.prop]), or
  • whether the closure type is a standard-layout class ([class.prop]).

所以大小的变化是可以的,但必须这样做才能使 is_empty_v<lambda_that_captures_stateless_lambda> 不是 true(因为这是一种可观察到的行为)


要“手动”应用此优化,您可以不调用 lambda comparator(left, right),而是默认构造闭包类型的某些类型并调用它 (decltype(comparator){}(left, right))。我已经在这里实现了:https://godbolt.org/z/73M1Gd3o5