【问题标题】:How store polymorphic closures?如何存储多态闭包?
【发布时间】:2013-10-29 23:32:17
【问题描述】:

C++1y 提供多态 lambda(即,使用 auto 作为参数类型的一部分):

int         f(int);
double      f(double);
std::string f(const std::string&);

auto funcObj = [](const auto& param){ return f(param); }

存储 lambda 生成的闭包很容易,如图所示:只需使用 auto 变量。但是假设我想创建一个vector 的此类对象。 vector 持有什么类型?通常的答案是使用std::function,但这在这种情况下不起作用,因为AFAIK没有多态std::function这样的东西,也就是说,这在C++ 1y中是不合法的:

std::vector<std::function<auto(const auto&)>> vecOfPolymorphicClosures;

如果这是合法的,那么你可以做一些事情,比如创建一个回调容器,每个回调容器都可以用任何一组参数调用,每个回调都可以返回一个依赖于传递参数类型的类型。至少在理论上,任何给定回调的结果都可以存储在auto 变量中。

两个问题:

  • 在 C++1y 中是否可以声明一个变量或容器来保存不同类型的多态 lambda(boost::any 之类的除外)?
  • 是否有理由希望这样的事情成为可能,或者这种事情与静态类型不兼容?

【问题讨论】:

  • 对于有限的类型列表,当然。无限?
  • 理想情况下是无限列表,是的。例如,我希望能够创建一个容器,该容器可以容纳从采用相同数量参数的多态 lambda 创建的任何闭包。从概念上讲,这与std::function 并没有太大区别,std::function 可以保存任何可调用的任何签名,并且与目标签名兼容。
  • decltype(funcObj): std::vector&lt;decltype(funcObj)&gt;std::vector&lt;decltype([](const auto&amp; x){return x;})&gt; 怎么样?可能不会。
  • @John:您的方法将只允许持有一种类型的关闭。这不会很有用,因为每个闭包都有一个唯一的类型。
  • 为了您的目的,Boost.Functional/OverloadedFunction 对于多态函子就像 Boost.Function 对于单态函子一样。如果一切都失败了,我会推荐 Boost.TypeErasure 作为更多自定义类型擦除的起点。

标签: c++ lambda polymorphism c++14


【解决方案1】:

没有。也许吧。

对于您的特定情况,您的 lambda 只是实例化时已知的单个函数 f 的覆盖集。可以通过类型擦除创建和传递覆盖集对象,而不会出现太大问题。您只需要手动枚举覆盖并将其提供给覆盖集。

因此,如果您的目标只是拥有一个作为f 覆盖集的对象,是的,您可以这样做。见"Manual" signature overload resolution——在乱七八糟的上面加上一些类型的擦除,鲍勃就是你的叔叔。

一般情况下,您有一些 auto lambda,其中包含任意代码,没有。

设想这个问题的方法是想象一个用你的 lambda 编译的 DLL 或共享库,第二个 DLL 或共享库包含 function 类似对象,以及其他一些想要调用它的 DLL 或共享库。

当您调用 function 时发生的行为取决于 lambda 的定义您要在任意程度上调用它的类型。

为了使其工作,几乎完整的运行时编译模型必须在创建 lambda 的 DLL 和调用它的类型的 DLL 中都可用,并且该运行时编译模型将必须兼容。

这既不是 C++ 标准所要求的,如果是这样的话,事情会变得更加复杂,并且会消除优化机会。

现在,并非一切都是绝望的。

如果有一些你想支持的固定类型列表,可以编写多态function 签名。这基本上是上面“覆盖集”解决方案的一个特例,甚至可以使用它来编写。

另一方面,如果您愿意键入擦除参数的属性到您的 lambda,并键入擦除,并返回一些统一类型(无论是 boost::anyboost::variant 或其他),您可以这样做某物。您编写一个类型擦除对象类型,并将其公开。然后你有一个std::function&lt; boost::any(type_erasure_object) &gt;,并且转换发生在调用之外,在你处理所述类型擦除对象的调用中。

使用类型擦除对象选择重载是很棘手的,因为 C++ 编译器无法帮助您生成要考虑的重载列表。如果您手动收集该列表,您甚至可以键入要选择的重载。

取消它是可能的,但我以前没有写过。这一切的替代方案都容易得多。

我不考虑使用类型擦除的情况来解决这个问题,因为它会阻止某些类型的优化。但从理论上讲,这意味着您可以使用几乎任意类型。

类型擦除对象必须向最终用户公开,并且它必须擦除 每一个您插入std::vector 需要的每个 lambda 类型信息知道。因此,在某些情况下,这会显着限制您在 std::vector 中存储的 lambda。

有关如何键入擦除几乎任意对象的示例,请查看 boost 类型擦除。

最后,您所要求的很少是问题的实际要求。您最好描述一下您的实际问题,几乎可以肯定它的解决方案不像上述那样深奥。

【讨论】:

  • 关于你的最后一段,我认为你忽略了单态 lambda,存储它们很简单:只需使用 function 对象,其签名对应于由拉姆达。对于多态 lambda,似乎没有办法将它们存储在容器中而不放弃对任意参数类型的支持。鉴于多态 lambda 是一项新功能,我认为尝试了解它们可以提供的解决方案的设计空间很重要。该设计空间似乎排除了多态 lambda 的容器。
  • @KnowItAllWannabe 多态 lambda 调用运算符不是调用运算符:它是用于生成调用运算符的 template。通过“存储”,您可能是指“类型擦除”,因为存储多态 lambda 很容易。是的,C++ 不支持template 代码生成的全上下文类型擦除。为此,您必须存储双方的几乎整个解析树以供将来任何链接时间使用(例如,加载 DLL 时,或者甚至在编译单元之间链接时)。您可能很少需要此功能,而且费用不会微不足道。
  • 如果存储多态 lambda 很容易,那我一定是遗漏了一些东西。如何将多态 lambda 存储为非静态数据成员?只涉及一种类型,所以我不需要类型擦除。使用function 对象意味着我放弃了 lambda 的多态性。
  • @KnowItAllWannabe auto a = [](auto x){}; 在自动存储中存储多态 lambda。 template&lt;typename T&gt;struct foo{T f;}; 可以在foo&lt;...&gt;::f 中存储一个多态 lambda,这是一个非静态数据成员,make_foo 创建函数可以让您轻松创建它。只有类型擦除使它变得困难(希望将其视为仅基于其多态签名的类型,这不是它的类型,而是它的类型擦除视图)。 std::function 是一种类型擦除工具,可以擦除标准 lambda 的大部分类型信息:签名不是类型。
  • 使用模板结构来保存多态 lambda 很好,谢谢。赞成。
【解决方案2】:

我要存储的是不同类型的对象,但每个对象都可以使用一组可能无限的参数类型来调用。

翻译单元A:

// a.cpp
#include <cassert>

std::vector<magical_type> v;

struct lives_in_a { int i; };

// defined in TU B:
void prepare();

int main()
{
    prepare();
    assert( v.front()(lives_in_a { 42 }) == 42 );
}

翻译单元B:

// b.cpp

struct lives_in_b {
    template<typename Anything>
    int operator()(Anything const& a) const
    { return a.i; }
};

void prepare()
{
    // ignore global initialization order fiasco for the sake
    // of the argument
    extern std::vector<magical_type> v;
    v.push_back(lives_in_b {});
}

lives_in_b::operator()&lt;lives_in_a&gt; 在何时何地被实例化,以便它可以被调用?

v.front() 使用参数lives_in_a {} 调用时?在那种情况下,看不到lives_in_b 的定义,甚至无法实例化。

何时调用v.push_back(lives_in_b {})?在这种情况下,看不到 lives_in_a 的定义,所以没有什么可以做的实例化。

这证明了 C++ 编译模型和模板实例化工作方式的特定组合不允许这种特定的愿望。它与静态类型关系不大。

【讨论】:

  • 我认为您的意思是在main 中致电prepare。除此之外,还有 C++ 的链接时实例化模型允许这种代码同时编译和链接,并且某些编译器支持此类模型。我不知道它们是否需要标准 C++ 支持。不过,你的观点是好的。我现在将其标记为有用,并将其标记为已接受的答案,除非进一步讨论带来新信息。
  • @KnowItAllWannabe 已修复。模板的单独编译确实在很大程度上被放弃了。并不是说在任何情况下它都对概述的特定场景没有帮助。
【解决方案3】:

所谓的generic lambda的类型是一个类类型,其成员模板为operator()。当需要转换时,必须知道实际类型。对于非捕获的泛型 lambda,当前的草案标准甚至包含一个示例:

auto glambda = [](auto a) { return a; };
int (*fp)(int) = glambda;

这与从普通函数模板形成函数指针没有什么不同。

对于一般的通用 lambda,我想期望可调用对象的转换将触发正确的模板特化,因此 std::function&lt;int(int)&gt; f(glambda); 应该可以正常工作。

【讨论】:

  • 我同意带有特定签名的 std::function 可以保存多态闭包,但是可以使用 int、std::stringstd::regex 等调用多态闭包。我不当我存储闭包时,我不想失去那种普遍性,而 std::function 并没有提供这一点,AFAIK。
  • @KnowItAllWannabe:那么您只是在问与“如何将模板存储在容器中以便我仍然可以任意专门化它”相同的问题。那时通用 lambda 并不真正相关。
  • 我不是在问如何将模板存储在容器中。我要存储的是不同类型的对象,但每个对象都可以使用一组可能无限的参数类型来调用。
  • @KnowItAllWannabe:本质上是一回事!
  • 是的。本质上,这些只是隐式模板。
猜你喜欢
  • 2021-09-05
  • 1970-01-01
  • 1970-01-01
  • 2015-03-06
  • 1970-01-01
  • 1970-01-01
  • 2017-09-12
相关资源
最近更新 更多