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

Common type of lambdas with same signature and captures

本文关键字:常见 lambda 类型      更新时间:2023-10-16

假设我有一些具有完全相同捕获完全相同签名的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] :

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

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

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


示例与提升::变体:

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

演示

注意:

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

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

std::function<>是默认可构造的。

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

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

记住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会给您的。

如果您愿意远离 lambdas,更简单的解决方案是具有三个不同成员函数的单个类,以及指向该类的成员函数的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";
}

它不存在。

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

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

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