【问题标题】:Common type of lambdas with same signature and captures具有相同签名和捕获的常见 lambda 类型
【发布时间】:2015-02-01 13:29:00
【问题描述】:

假设我有一些具有完全相同的捕获完全相同的签名的 lambda。

int captured;
auto l0 = [&captured](int x){ captured += x; }; 
auto l1 = [&captured](int x){ captured -= x; };
auto l2 = [&captured](int x){ captured = x + 1; };

现在,假设我需要将这些 lambda 存储在 std::vector 中,以便在运行时调用它们。

我不能使用原始函数指针,因为捕获的变量会强制 lambda 成为仿函数,而不是常规函数。

我可以使用std::function,但这有点矫枉过正,因为我确定所有的 lambda 表达式都具有相同的签名和相同的捕获。由于 std::function 支持具有相同签名但捕获不同的 lambda,我(很可能)支付了额外的运行时成本,可能是 (?) em> 避免。

std::vector<decltype(l0)> v0; // Ok
v0.emplace_back(l0);          // Ok
v1.emplace_back(l1);          // Nope: `decltype(l0) != decltype(l1)`
v2.emplace_back(l2);          // Nope: `decltype(l0) != decltype(l2)`

我想找出所有 lambda 之间的共同类型,但 std::common_type 不起作用。

// Nope: does not compile
using LCT = std::common_type_t<decltype(l0), decltype(l1), decltype(l2)>;

基本上,我需要一个介于原始函数指针和std::function 之间的东西。这样的东西存在吗?而且……这样的事情真的可以实现吗?

【问题讨论】:

  • 每个 lambda 都有一个与其他任何东西无关的唯一类类型。即使是写两次的同一个 lambda 表达式也有不同的类型。如果可能的 lambda 类型的数量是有限的,请考虑使用标记联合。
  • “很可能” 听起来不像您通过测量发现std::function 对于您的用例而言太慢了...
  • @T.C.:我明白你的意思。但本能地在我看来l0l1l2 确实具有相同的内存布局(尽管标准可能无法保证)。困扰我的是,如果我对具有相同内存布局的 lambda 表达式是正确的,那么就没有办法(除非使用极其不安全的 reinterpret_cast 或使用 std::function 支付额外费用)将它们组合在一起。
  • @VittorioRomeo 通过完全相同的捕获,您是指在[] 之间声明的捕获列表的相同形式,还是您想要的所有 lambda存储在该向量中将捕获完全相同的变量,具有完全相同的值,因此每个闭包对象实际上将存储相同的数据值,并为每个闭包对象存储这些值对象是多余的?
  • 为什么不转向无捕获 lambda,然后通过引用 lambda 显式传入 captured?然后你有一个指向简单的双参数函数的指针向量。

标签: c++ c++11 types lambda c++14


【解决方案1】:

C++ 标准部分 § 5.1.2 [expr.prim.lambda] :

lambda 表达式的类型(也是闭包对象的类型)是一个唯一,未命名的非联合类类型——称为闭包类型

每个 lambda 都有不同的类型:l0l1l2 没有共同的类型。

所以考虑一个变体类型的std::vector&lt;&gt;,例如boost.variant(如果你知道 lambda 类型的集合),或者使用 std::function&lt;&gt;,这在这里似乎也很合适。


示例boost::variant

int main () {
    int captured = 42;
    auto l0 = [&captured](int x){ captured += x; }; 
    auto l1 = [&captured](int x){ captured -= x; };
    auto l2 = [&captured](int x){ captured = x + 1; };

    std::vector<boost::variant< decltype(l0), decltype(l1), decltype(l2)>> variant;
    variant.push_back(l0);
    variant.push_back(l1);
    variant.push_back(l2);

    auto f =  boost::get<decltype(l1)>(variant[1]);

    int i = 1;
    f(i);
    std::cout << captured;
}

Demo

注意:

正如 Johannes Schaub 所指出的,像这样的 lambda 变体不是默认可构造的,即你不能写:

boost::variant< decltype(l0), decltype(l1), decltype(l2)> v;

std::function&lt;&gt; 是默认可构造的..

【讨论】:

  • 我想提请注意 variant 不能是 "empty" 的事实,因此对于这样的 lambda 变体,它不是默认可构造的。对于std::function,您将能够默认构造它。
【解决方案2】:

根据您对我的评论的回答,我认为这是(非常粗略地)您想要的:

#include <iostream>
#include <type_traits>
#include <vector>

template<typename L, typename R, typename... Args> struct lambda_hack
{
    using storage_type = std::aligned_storage_t<sizeof(L), std::alignment_of<L>::value>;
    static storage_type storage;
    static void init_data(const L& arg) { new(&storage) L(arg); }
    template<typename LL> static R call_target(Args... args) { return reinterpret_cast<LL&>(storage)(args...); }

    template<typename LL> lambda_hack(LL&&) : target(call_target<LL>) { }
    using target_type = R(*)(Args...);
    target_type target;
    R operator()(Args... args) const { return target(args...); }
};

template<typename L, typename R, typename... Args> 
typename lambda_hack<L, R, Args...>::storage_type lambda_hack<L, R, Args...>::storage;

int main()
{
    int captured = 7;
    auto l0 = [&captured](int x){ captured += x; }; 
    auto l1 = [&captured](int x){ captured -= x; };
    auto l2 = [&captured](int x){ captured = x + 1; };

    using lhack = lambda_hack<decltype(l0), void, int>;
    lhack::init_data(l0);
    std::vector<lhack> v{l0, l1, l2};
    for(auto& h : v)
    {
        std::cout << "'captured' before: " << captured << '\n';
        h(3);
        std::cout << "'captured' after: " << captured << '\n' << '\n';
    }
    std::cout << captured << '\n'; // prints '4', as expected
}

std::vector 中存储的函子只是一个非成员函数指针的大小。实际捕获的数据单独存储一次。在这样的仿函数上调用 operator() 只需通过该指针进行一次间接调用(比虚函数调用更好)。

它在 C++14 模式下的 GCC 4.9.1 和 Clang 3.5.0 以及 VC++ 2013 上编译和运行。

将其视为您在生产中实际使用的 alpha 版本。它需要改进(例如,它没有正确破坏静态存储)。我想先看看这是否确实是您想要的。

首先要解决的问题可能是storage 不应该是static。由于一组这样的 lambda 本质上是密切相关的,因此您可能希望将它们存储在一个容器中,正如您在问题中提到的那样。由于storage 需要在该容器存在期间一直可用,因此我将其存储在容器本身中(子类std::vector,也许?...)并在容器被销毁时销毁其内容。

【讨论】:

  • 非常有趣。当我有空闲时间并分享我的结果时,我会尝试一下。谢谢!
  • @VittorioRomeo 据我所知,std::function 有很好的实现,只需要通过函数指针调用operator(),就像我的解决方案一样。在最坏的情况下,它是一个虚函数调用或类似的东西(两个间接),这显然更慢,但不是灾难性的。我不确定,但我认为libstdc++ 有这个优化。所以,函数调用本身的性能很可能是相同的或者比较接近的。我不认为你可以做得更好,并且仍然有一个相当通用的解决方案。
  • @VittorioRomeo 但是,练习的目的是利用闭包对象中存储的数据对于所有 lambda 都是相同的,并且只存储一次。这应该适用于大小合理的数组(例如,从一百万个条目开始,这应该使事情足够可见),尤其是对于不太小的捕获(在 32 位机器上至少 32 个字节) )。首先应该显示的是堆分配的差异:我的解决方案没有,而std::function 需要将每个闭包对象单独存储在堆上。
  • @VittorioRomeo 在遍历向量和调用仿函数时,一切都应该更好地适合我的解决方案。毕竟它只是一个指针,而std::function对象更大,而闭包对象数据本身在一个地方也只有32个字节,而每次都需要扫描32 MB(更不用说他们可能不会)顺序的,因为它们是单独的堆分配 - 取决于您的分配模式)。在数组中移动(重新排序调用)应该也会显示性能差异,但可能没有那么大。
  • @VittorioRomeo 我认为这些是您应该期望看到性能差异的场景。这就是您所要求的 - 利用闭包对象中的公共数据,但保留对该数据调用不同函数的能力,所有这些都使用 lambda 表达式表示法。任何进一步的优化都需要摆脱函数指针,我认为通常这样做的唯一方法是在编译时知道将要调用哪些仿函数以及以什么顺序调用,但这是一个不同的问题,没有什么可做的使用std::function
【解决方案3】:

记住什么是 lambda:可以用 C++98 手动编写的函数对象的简写。

您的三个 lambda 等效于以下内容:

int captured;

struct l0_t {
    int& captured;
    l0_t(int& _captured) : captured(_captured) {}
    void operator()(int x) const { captured += x; }
} l0(captured);

struct l1_t {
    int& captured;
    l1_t(int& _captured) : captured(_captured) {}
    void operator()(int x) const { captured -= x; }
} l1(captured);

struct l2_t {
    int& captured;
    l2_t(int& _captured) : captured(_captured) {}
    void operator()(int x) const { captured = x + 1; }
} l2(captured);

鉴于此,如果您希望能够以多态方式处理这三个对象,那么您需要某种虚拟调度,而这正是 std::functionboost::variant 将为您提供的。

如果您愿意放弃 lambda,一个更简单的解决方案是使用具有三个不同成员函数的单个类,以及指向该类成员函数的指针 vector,因为没有理由为向量对捕获的对象有自己的引用:

struct f {
    int& captured;
    f(int& _captured) : captured(_captured) {}
    void f0(int x) const { captured += x; }
    void f1(int x) const { captured -= x; }
    void f2(int x) const { captured = x + 1; }
};

int captured = 0;
f multiplex(captured);
std::vector<decltype(&f::f0)> fv { &f::f0, &f::f1, &f::f2 };
for (auto&& fn : fv) {
    (multiplex.*fn)(42);
    std::cout << captured << "\n";
}

【讨论】:

    【解决方案4】:

    它不存在。

    您可以使用std::function。您可以使用boost::variant。或者您可以编写自己的类型擦除类型。

    存储boost::variant 或重新实现它并公开特定operator() 签名的one_of_these_function 使用变体上的访问者调用正确的方法将合理有效地解决您的问题。

    另一个稍微疯狂的选择是编写你自己的函数,比如基于“尽可能快的委托”技术的类,假设上面的 lambdas 是一个指针的大小,可以被视为可简单复制并使用 tomfoolery 伪存储它们并在指向所述存储指针的指针上调用operator()。我可以告诉你它有效,但它可能对语言不礼貌。

    【讨论】:

      猜你喜欢
      • 2020-09-04
      • 2020-02-12
      • 1970-01-01
      • 1970-01-01
      • 2015-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多