Lambda表达式作为类模板参数

74

lambda表达式可以用作类模板参数吗?(注意这是一个非常不同的问题,与此问题不同,该问题询问的是lambda表达式本身是否可以成为模板。)

我想知道是否可以像以下这样做:

template <class Functor> 
struct Foo { };
// ...
Foo<decltype([]()->void { })> foo;

这在一些情况下会很有用,比如一个类模板有各种参数,如equal_to或其他通常被实现为单行函数对象的参数。例如,假设我想要实例化一个使用自定义等式比较函数的哈希表。我希望能够像这样说:

typedef std::unordered_map<
  std::string,
  std::string,
  std::hash<std::string>,
  decltype([](const std::string& s1, const std::string& s2)->bool 
    { /* Custom implementation of equal_to */ })
  > map_type;

但是我在GCC 4.4和4.6上测试了一下,似乎不起作用,这显然是因为由lambda表达式创建的匿名类型没有默认构造函数。(我记得boost::bind也有类似的问题。)标准草案是否允许这样做,还是我错了,允许这样做,只是GCC在实现上落后了?


正如其他人所评论的那样,这在c++20中是可能的。以下是一个使用它来传递任意数据到模板并绕过NTTP限制的示例:https://compiler-explorer.com/z/Ed5ob7jes - mnesarco
5个回答

63

截至C++20,这个答案已经过时。C++20引入了无状态lambda在未求值的上下文中1:

最初设计此限制是为了防止lambda出现在函数签名中,因为lambda需要具有唯一类型,这将导致符号重载方面的问题。但是,该限制比必要的更强,并且确实可以在不使用该限制的情况下实现相同的效果。

仍然存在一些限制(例如lambda仍然无法出现在函数签名中),但所描述的用例现在完全有效,不再需要声明变量。


我在问您是否能够执行以下操作:

Foo<decltype([]()->void { })> foo;
不行,因为lambda表达式不能出现在未求值的上下文中(例如decltype和sizeof等)。C++0x FDIS,5.1.2 [expr.prim.lambda] p2 lambda表达式的求值会产生一个prvalue临时对象(12.2),称为闭包对象。lambda表达式不得出现在未求值的操作数中(Clause 5)。[注意:闭包对象的行为类似于函数对象(20.8)。—结尾注释]。
您需要先创建一个特定的lambda,然后再对其使用decltype:
auto my_comp = [](const std::string& left, const std::string& right) -> bool {
  // whatever
}

typedef std::unordered_map<
  std::string,
  std::string,
  std::hash<std::string>,
  decltype(my_comp)
  > map_type;

那是因为每个lambda-derived闭包对象可能具有完全不同的类型,毕竟它们就像匿名函数一样。


3
@Xeo,“匿名函数”其实并不是真正的函数。这句话应该大声说出来,因为它很容易让人感到困惑。 - There is nothing we can do
1
@There:与答案中提到的同一段落,但是p6:“对于没有lambda-capture的lambda表达式,闭包类型具有一个公共的非虚拟的非显式const转换函数,可以将其转换为具有与闭包类型的函数调用运算符相同参数和返回类型的函数指针。此转换函数返回的值应为一个函数的地址,当被调用时,它具有与闭包类型的函数调用运算符相同的效果。” - Xeo
2
@那里:第5.1.2节,第1、2和6段。第1和2段解释了Lambda创建函数对象(函数符)。第6段说,非捕获Lambda对应于普通的自由函数(使其能够存储在普通函数指针中)。 - Ben Voigt
1
@Ben:函数基本上总是一个函数指针,甚至可以被解引用(尝试使用(* main)()作为示例)。从解引用中获得的是一个函数标识符(我认为它被称为这个),它立即而悄无声息地转换回函数指针(你可以使用(********main)()来实现)。 - Xeo
1
@Ben 数组也会根据需要“衰减”成指针,但你仍然称它们为数组而不是指针。重点是 Lambda 表达式不是无名函数,而是无名对象。当你说“一个函数难道不只是一个对象吗...”时,我可以说一件事:这在 C++ 的意义下并非如此。 - There is nothing we can do
显示剩余11条评论

15

C++20的答案:是的!

你完全可以像这样做:

Foo<decltype([]()->void { })> foo;

由于C++20允许在未求值的上下文中使用无状态lambda表达式。


13

@Xeo已经向你解释了原因,因此我将提供一个解决方法。

在很多情况下,您不希望为闭包命名,在这种情况下,您可以使用 std::function,它是一种类型:

typedef std::unordered_map<
  std::string,
  std::string,
  std::hash<std::string>,
  std::function<bool(std::string const&, std::string const&)>
  > map_type;

请注意,它精确地捕获了函数的签名,不多也不少。

然后,在构建map时,您可以简单地编写lambda表达式。

请注意,对于unordered_map,如果您更改相等比较,则最好更改哈希以匹配行为。 相等的对象应具有相同的哈希值。


3
不太有用,因为std::function不包含实际lambda类型所具有的operator()行为。 - Ben Voigt
1
@BenVoigt:它有什么不同之处吗?我认为 std::function 基于 boost::function,它们的行为是相同的,不是吗? - Joseph Garvin
@Joseph:行为包含在特定的function实例中。但是模板仅使用类型,而不是实例,因此没有行为。哈希表实现将在每次想要调用函数对象时创建一个新实例,而function是抽象的--它无法被实例化。 - Ben Voigt
1
@BenVoigt:如果调用默认构造的function,它会抛出std::bad_function_call异常(基本上是无用的)。这意味着在构建map_type实例时,除了提供用于存储的类型之外,还应该提供一个比较函数实例,例如lambda表达式。请注意,比较对象仅在构建时构建一次,而不是在每次调用时都构建。如果您要求它具有状态,则可以是有状态的,而且该函数不是抽象的。真的。[请参阅文档。](http://en.cppreference.com/w/cpp/utility/functional/function) - Matthieu M.
1
@SohailSi:不,2011年的时候我还没有足够的先见之明去猜测C++20会包含什么。 - Matthieu M.
显示剩余4条评论

5

使用闭包是无法做到这一点的,因为状态不包含在类型中。

如果您的lambda表达式是无状态的(没有捕获变量),那么您应该没问题。在这种情况下,lambda表达式会衰减成普通函数指针,您可以将其用作模板参数,而不是某个lambda类型。

然而,gcc并不喜欢这样做。http://ideone.com/bHM3n


使用Xeo发布的解决方法,您可以使用捕获...您只需要将lambda传递给unordered_map构造函数(因为它无法自己构造)。 - ijprest
1
@ijpriest:我在Xeo的回答中没有看到任何传递lambda对象的内容。lambda对象是为了传递给decltype而创建的。这并不意味着更复杂的用法不可能,但我认为你需要将lambda放在全局范围内以命名模板实例,这会排除捕获的可能性。你可以像@DeadMG建议的那样使用std::function和在运行时指定的lambda对象的组合,但这会牺牲编译时多态的效率。 - Ben Voigt
2
@ijprest -- 不行,你不能使用Xeo发布的解决方法。尝试这样做会出现类似于这样的错误:error: use of deleted function ‘<lambda(int, int)>::<lambda>()’ - Periata Breatta
@PeriataBreatta:因此我使用了“传递lambda表达式...无法自行构造”的限定词。您需要调用unordered_map构造函数的长形式,该构造函数允许您将lambda的实例作为最后一个参数传递。 - ijprest
@ijprest - 是的...不幸的是,并非所有的模板类都有这样长的构造函数。 :( - Periata Breatta
@ijprest:当这个答案被写出来时,本地类型不允许作为模板参数。这就是为什么我之前评论说“需要将lambda放在全局范围内才能命名模板实例”。这个限制最终在新的C++标准中得到了解除,使得这种情况成为可能。 - Ben Voigt

0

你需要使用运行时抽象类型,例如 std::function,或者将该类型作为本地变量或模板类的一部分来创建。


2
我认为目标是让类型表达行为,而std::function仅表达签名。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接