lambda捕获的“空基础优化” - 标准禁止?为什么?

时间:2017-07-12 11:41:19

标签: c++ lambda c++17

我最近遇到过一种情况,我最终将大量嵌套的lambdas传递给build asynchronous computation chains

template <typename F>
struct node : F
{
    node(F&& f) : F{std::move(f)}
    {
    }

    template <typename FThen>
    auto then(FThen&& f_then)
    {
        return ::node{[p = std::move(*this), t = std::move(f_then)]()
        {   
        }};
    }
};

int main()
{
    auto f = node{[]{ }}.then([]{ }).then([]{ });
    return sizeof(f);
}   

我在lambda中捕获的所有对象都是为空,但最终对象的大小大于1:example on gcc.godbolt.org

如果我将node</* ... */>::then内的lambda更改为具有显式EBO的函数对象,则最终对象的大小将变为1。

template <typename P, typename T>
struct node_lambda : P, T
{
    node_lambda(P&& p, T&& t) : P{std::move(p)}, T{std::move(t)}
    {
    }

    void operator()()
    {
    }
};

template <typename FThen>
auto node</* ... */>::then(FThen&& f_then)
{
    return ::node{node_lambda{std::move(*this), std::move(f_then)}};
}

Live example on gcc.godbolt.org

我觉得这很烦人,因为我被迫:

  • 编写大量与lambda相当的样板代码。

  • 由于EBO之类的内容不适用于lambda捕获,因此需要支付额外的内存费用。

标准中是否有明确强制空lambda捕获占用额外空间的东西?如果是,为什么?

5 个答案:

答案 0 :(得分:10)

来自expr.prim.lambda.capture

  

对于通过复制捕获的每个实体,在闭包类型中声明一个未命名的非静态数据成员。

虽然这里的lambdas没有捕获:

auto f = node{[]{ }}.then([]{ }).then([]{ });

因此没有未命名的非静态数据成员,因此是空的,这不是then()实际使用的。它使用了这个:

return ::node{[p = std::move(*this), t = std::move(f_then)](){}};

lambda通过副本捕获tp,因此有两个未命名的非静态数据成员。每个.then()添加另一个成员变量,即使每个变量都是空的,因此节点的大小也会不断增加。

或者换句话说,空基础优化仅适用于基础,而lambdas的捕获不会创建基础,它会创建非静态数据成员。

答案 1 :(得分:4)

其他答案有原因,所以我不会重复。我将补充说,我能够将您的示例转换为基于继承的示例,而不需要太多样板。由于您在OP中执行公共继承,因此我选择删除c&tor;并进行聚合初始化。

它只需要两个演绎指南,使代码几乎与原始方案一样漂亮:

Live on Coliru

#include <utility>
#include <iostream>

struct empty {
    void operator()() {}
};

template <typename P, typename T>
struct node : P, T
{
    template <typename FThen>
    auto then(FThen&& f_then)
    {
        return ::node{std::move(*this), std::forward<FThen>(f_then)};
    }

    void operator()() {
        P::operator()();
        T::operator()();
    }
};

template <typename P>             node(P)    -> node<P, ::empty>;
template <typename P, typename T> node(P, T) -> node<P, T>;

int main()
{
    auto f = node{[]{ }}.then([]{ }).then([]{ });
    std::cout << sizeof(f);
}   

应用了EBO,您可以通过以下链接看到。

顺便说一句,由于我们正在移动*this,因此r值可能值node::then。只是为了避免任何肮脏。

答案 2 :(得分:4)

根据as-if规则和[expr.prim.lambda.closure]/2

  

实现可以定义闭包类型与不同的类型   如下所述,只要这不会改变可观察的行为   该计划除了改变:

     
      
  • 封闭类型的大小和/或对齐方式
  •   
  • 闭包类型是否可以轻易复制(Clause [class]),
  •   
  • 闭包类型是标准布局类(Clause [class])还是
  •   
  • 闭包类型是否为POD类(Clause [class])。
  •   

我没有看到任何阻止实现使用某种魔法来优化捕获的空变量的存储的内容。

那就是说,这样做会导致ABI休息,所以不要屏住呼吸。

另一方面,允许 - 或要求 - 实现使得捕获的空变量的类型成为闭包类型的 base 将是一个可怕的坏主意。考虑:

struct X { };
struct Y { };
void meow(X x);                     // #1
void meow(Y y);                     // #2
void meow(std::function<void()> f); // #3

template<class T, class U>
void purr(T t, U u) {
    meow([t = std::move(t), u = std::move(u)] { /* ... */ });
}

purr除了调用#3之外做任何其他事情都是疯了,但如果捕获可以成为基础,那么它可以调用#1,或#2,或者是不明确的。

答案 3 :(得分:3)

正如其他人所指出的那样,lambdas被指定为捕获成员变量而非基数。所以你运气不好。

假设你有一个 使用空基优化的元组。然后我们可以写一个帮手:

template<class Sig>
struct lambda_ebo_t;
template<class F, class...Args>
struct lambda_ebo_t<F(Args...)>:
  private std::tuple<Args...>,
  private F
{
  decltype(auto) operator()(){
    return std::apply( (F&)*this, (std::tuple<Args...>&)*this );
  }
  template<class...Ts>
  lambda_ebo_t( F f, Ts&&...ts ):
    std::tuple<Args...>( std::forward<Ts>(ts)... ),
    F( std::move(f) )
  {}
};

template<class F, class...Args>
lambda_ebo_t<F, std::decay_t<Args>...>
lambda_ebo( F f, Args&&...args ) {
  return {std::move(f), std::forward<Args>(args)...};
}

这是一堆样板,并且不完整(参考捕获可能无法正常工作,即使您使用std::ref),但它给了我们:

template <typename FThen>
auto then(FThen&& f_then)
{
    return ::node{lambda_ebo([](auto&& p, auto&& t)
    {   
    }, std::move(*this), std::move(f_then))};
}

我们将数据存储在lambda之外,并将其作为参数传递给lambda。存储使用EBO。

无需为每个lambda编写一个自定义EBO类,只需要几个箍就可以在需要启用EBO的lambda时跳过。

这是一个没有使用元组的,但它不支持像int这样的基础类型或其他你无法从中得到的东西:

template<class Sig>
struct lambda_ebo_t;
template<class F, class...Args>
struct lambda_ebo_t<F(Args...)>:
  private Args...,
//  private std::tuple<Args...>,
  private F
{
  decltype(auto) operator()(){
    //return std::apply( (F&)*this, (std::tuple<Args...>&)*this );
    return ((F&)(*this))((Args&)*this...);
  }
  template<class...Ts>
  lambda_ebo_t( F f, Ts&&...ts ):
    Args(std::forward<Ts>(ts))...,
    F( std::move(f) )
  {}
};

template<class F, class...Args>
lambda_ebo_t<F(std::decay_t<Args>...)>
lambda_ebo( F f, Args&&...args ) {
  return {std::move(f), std::forward<Args>(args)...};
}

Live example,带有此测试代码:

auto test = lambda_ebo( [](auto&&...args){std::cout << sizeof...(args) << "\n";}, []{} , []{}, []{}, []{}, []{}, []{}, []{}, []{}); //
std::cout << "bytes:" << sizeof(test) << "\n";
std::cout << "args:";
test();

sizeof(test)1,它“捕获”了8个参数。

答案 4 :(得分:2)

在以下情况下,空基础优化适用于我

#include <utility>

template <typename F>
class Something : public F {
public:
    Something(F&& f_in) : F{std::move(f_in)} {}
};

int main() {
    auto something = Something{[]{}};
    static_assert(sizeof(decltype(something)) == 1);
}

此处的实例https://wandbox.org/permlink/J4m4epDUs19kp5CH

我的猜测是,它在你的情况下不起作用的原因是你在then()方法中使用的lambda实际上不是空的,它有成员变量 - 你捕获中列出的变量。所以那里没有真正的空基地。

如果您将代码的最后一行更改为仅返回node{[]{}},那么它可以正常工作。 .then()使用的lambdas没有实现为&#34;空&#34;类。

然而在显式结构的情况下,它本身没有成员变量,只有它派生的类,因此空基优化可以在那里工作。