具有相同签名和捕获的常见 lambda 类型

Common type of lambdas with same signature and captures

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

的 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,我 (很可能) 支付了额外的运行时成本,这可能是(?) 避免了。

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 之间的东西。 是否存在类似的东西?而且......可以实际实施类似的东西吗?

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

The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non union class type — called the closure type

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

因此请考虑 std::vector<> 变体类型,例如boost.variant(如果你知道 lambda 类型的集合),或使用 std::function<>,这在这里似乎也合适。


示例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<> 是默认可构建的..

记住什么是 lambda:shorthand 可以用 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,一个更简单的解决方案是一个 class 具有三个不同的成员函数,以及一个 vector 指向 [=23] 成员函数的指针=],因为没有理由让向量的每个元素都有自己对捕获对象的引用:

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";
}

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

#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,也许?...)并在容器被销毁时销毁其内容.

不存在。

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

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

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