C++14 中的 Lambda C++14 对 Lambda 表达式增加了两个重要的功能:
此外,该标准还更新了一些规则,例如:
Lambda 的默认参数
以 auto 作为返回类型
这些特性可以解决 C++11 中出现的几个问题。你可以在 N4140 和 Lambdas 中查看相关细节。此外,在本章,你将会了解到:
捕获非静态数据成员
用现代的技术替代旧的函数风格工具,如 std::bind1st
LIFTING 惯用法
递归 Lambda
Lambda 的默认参数 让我们从一些较小的更新开始:
在 C++14 中,可以在函数调用中使用默认参数。这是一个小功能,但可以使 Lambda 更像一个常规函数。
1 2 3 4 5 6 7 8 #include <iostream> int main () { const auto lam = [](int x = 10 ) { std::cout << x << '\n' ; }; lam (); lam (100 ); }
在上例中,我们调用了 Lambda 两次。第一次没有传入任何参数,因此它使用了默认值 x = 10。第二次我们传入了 100。
有趣的是 GCC 和 CLang 编译器早在 C++11 时就支持该特性了。
返回类型推导 如之前章节所述,简单 Lambda 表达式的返回类型可由编译器自动推导。C++14 将这一特性扩展至常规函数,允许使用 auto 作为返回类型:
1 2 3 4 5 auto myFunction () { const int x = computeX (...); const int y = computeY (...); return x + y; }
对于以上代码,编译器将会推导出 int 作为返回类型。
对于 Lambda 表达式而言,C++14 意味着它们遵循与 auto 返回类型函数相同的规则。我们查看标准文档 [expr.prim.lambda#4] 中的定义:
Lambda 表达式的返回类型为 —— 若提供了尾置返回类型 (trailing-return-type),则替换该 auto 类型;否则按 [dcl.spec.auto] 章节所述,通过 语句推导返回类型。
当存在多个 return 语句时,所有语句必须推导出相同的类型。
1 2 3 4 auto foo = [] (int x) { if (x < 0 ) return x * 1.1f ; return x * 2.1 ; };
上述代码无法通过编译:第一个 return 语句返回 float 类型,而第二个返回 double 类型。编译器无法自行裁决,必须由开发者明确指定单一返回类型。
虽然数值类型(如整型/浮点型)的自动推导有其便利性,但返回类型推导的真正价值体现在更重要的场景中。该特性在模板编程和处理”未知类型”时具有关键作用。
例如,Lambda 闭包类型本身是匿名类型,我们无法在代码中显式声明。若需要从函数返回一个 Lambda,该如何指定返回类型?在 C++14 之前,只能通过 std::function 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <functional> #include <iostream> std::function<int (int ) > CreateMulLambda (int x) { return [x](int param) noexcept { return x * param; }; } int main () { const auto lam = CreateMulLambda (10 ); std::cout << sizeof (lam); return lam (2 ); }
然而,上述解决方案并不简洁。它要求开发者显式指定函数签名,甚至需要额外包含 <functional> 头文件。如果你回顾 C++11 章节的内容,std::function 是一个重型对象(在 GCC 9 中 sizeof 显示为 32 字节),它需要复杂的内部机制才能处理任意可调用对象。
得益于 C++14 的改进,我们现在可以简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Ex3_3: Auto return type deduction for lambdas. #include <iostream> auto CreateMulLambda (int x) noexcept { return [x](int param) noexcept { return x * param; }; } int main () { const auto lam = CreateMulLambda (10 ); std::cout << sizeof (lam); return lam (2 ); }
现在我们可以完全依赖编译时类型推导,无需任何辅助类型。在 GCC 中,lambda 表达式的大小 sizeof(lam) 仅为 4 字节,其开销远低于 std::function 方案。请注意,由于该函数不可能抛出任何异常,我们还可以将 CreateMulLambda 标记为 noexcept ——而返回 std::function 时则无法做到这一点。
带初始化器的捕获 现在带来更重要的更新! 如你所知,lambda 表达式可以捕获外部作用域的变量。编译器会展开捕获语法,在闭包类型中创建对应的非静态数据成员。
在 C++14 中,你可以直接在捕获子句中初始化新的数据成员,随后在 Lambda 函数体内访问这些变量。此特性被称为:带初始化器的捕获(capture with an initialiser)或广义 Lambda 捕获(generalised Lambda capture)。
例如:
1 2 3 4 5 6 7 8 9 10 11 #include <iostream> int main () { int x = 30 ; int y = 12 ; const auto foo = [z = x+y]() { std::cout << z << '\n' ; }; x = 0 ; y = 0 ; foo (); }
输出:42
在上面的例子中,编译器会生成一个新的数据成员并用 x + y 初始化它。这个新变量的类型推导方式,等同于在该变量前使用 auto 关键字。例如:
auto z = x + y;
总结来说,前例中的 Lambda 表达式会被解析为以下(简化后的)可调用类型:
1 2 3 4 5 6 struct _unnamedLambda { void operator () () const { std::cout << z << '\n' ; } int z; } someInstance;
当 Lambda 表达式被定义时,z 会被直接初始化(使用 x + y 的值)。 请牢记:新变量是在定义 Lambda 时初始化,而非调用时初始化。
因此,如果在创建 Lambda 后修改 x 或 y 的值,变量 z 不会改变。在示例中可以看到,即便在定义 Lambda 后立即修改了 x 和 y 的值,输出仍然是 42,因为 z 早已完成初始化。
通过初始化器创建变量非常灵活,例如你还可以创建对外部作用域变量的引用。
1 2 3 4 5 6 7 8 9 10 #include <iostream> int main () { int x = 30 ; const auto foo = [&z = x]() { std::cout << z << '\n' ; }; foo (); x = 0 ; foo (); }
这一次,变量 z 是 x 的引用。它的创建方式等同于这样写:
auto &z = x;
如果你运行这个例子,会看到第一行输出 30,但第二行显示 0。这是因为我们捕获的是引用,所以当你修改被引用的变量时,z 对象的值也会随之改变。
限制条件 需要注意的是,虽然可以通过初始化器捕获引用,但不能使用右值引用 && 语法。因此以下代码是无效的:
[&&z = x] // 无效语法!
该特性的另一个限制是不支持参数包。请参考标准文档 [expr.prim.lambda] 第 24 节中的说明:
简单捕获后接省略号属于包展开([temp.variadic])。而初始化捕获后接省略号的语法是 ill-formed(非法的)。
换句话说,在 C++14 中,你无法写出:
1 2 3 4 template <class ... Args>auto captureTest (Args... args) { return Lambda = [...capturedArgs = std::move (args)](){};
不过,这一语法将在 C++20 中成为可能,具体可参见本标准文档第 114 页的相关章节。
现有问题的改进 移动 1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> #include <memory> int main () { std::unique_ptr<int > p (new int {10 }) ; const auto bar = [ptr = std::move (p)] { std::cout << "pointer in lambda: " << ptr.get () << '\n' ; }; std::cout << "pointer in main(): " << p.get () << '\n' ; bar (); }
1 2 pointer in main(): 0 pointer in lambda: 0x1413c20
std::function 的一个陷阱1 2 3 std::unique_ptr<int > p (new int {10 }) ;std::function<void ()> fn = [ptr = std::move (p)]() { };
优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Ex3_8: Creating a string for a lambda. #include <algorithm> #include <iostream> #include <string> #include <vector> int main () { using namespace std::string_literals; const std::vector<std::string> vs = { "apple" , "orange" , "foobar" , "lemon" }; const auto perfix = "foo" s; auto result = std::find_if (vs.begin (), vs.end (), [&prefix](const std::string& s) { return s == prefix + "bar" s; } ); if (result != vs.end ()) std::cout << prefix << "-something found!\n" ; result = std::find_if (vs.begin (), vs.end (), [saveString = prefix + "bar" s](const std::string& s) { return s == savedString; } ); if (result != vs.end ()) std::cout << prefix << "-something found!\n" ; }
捕获类的数据成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <algorithm> #include <iostream> struct Baz { auto foo () const { return [s = s] { std::cout << s << '\n' ; }; } std::string s; }; int main () { const auto f1 = Baz{"abc" }.foo (); const auto f2 = Baz{"xyz" }.foo (); f1 (); f2 (); }
泛型 Lambda 1 2 3 4 const auto foo = [](auto x, int y) { std::cout << x << ", " << y << '\n' ; };foo (10 , 1 );foo (10.1234 , 2 );foo ("hello world" , 3 );
1 2 3 4 5 6 struct { template <typename T> void operator () (T x, int y) const { std::cout << x << ", " << y << '\n' ; } } someInstance;
const auto fooDouble = [](auto x, auto y) { /*...*/ };
1 2 3 4 struct { template <typename T, typename U> void operator () (T x, U y) const { } } someOtherInstance;
可变泛型参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> template <typename T>auto sum (T x) { return x; }template <typename T1, typename ... T>auto sum (T1 s, T... ts) { return s + sum (ts...); }int main () { const auto sumLambda = [](auto .. args) { std::cout << "sum of: " << sizeof ...(args) << " numbers\n" ; return sum (args...); }; std::cout << sumLambda (1.1 , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , 9.9 ); }
1 2 3 4 struct __anonymousLambda { template <typename ... T> void operator () (T... args) const { } };
泛型 Lambda 的完美转发 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <string> void foo (const std::string&) { std::cout << "foo(const string&)\n" ; }void foo (std::string&&) { std::cout << "foo(string&&)\n" ; }int main () { const auto callFoo = [](auto && str) { std::cout << "Calling foo() on: " << str << '\n' ; foo (std::forward<decltype (str)>(str)); }; const std::string str = "Hello World" ; callFoo (str); callFoo ("Hello World Ref Ref" ); }
1 2 3 4 Calling foo() on: Hello World foo(const string&) Calling foo() on: Hello World Ref Ref foo(string&&)
1 2 3 4 5 template <typename T>void callFooFunc (T&& str) { std::cout << "Calling foo() on: " << str << '\n' ; foo (std::forward<T>(str)); }
正确类型的推导 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <algorithm> #include <iotream> #include <map> #include <string> int main () { const std::map<std::string, int > numbers { { "one" , 1 }, { "two" , 2 }, { "three" , 3 } }; std::for_each(std::begin (numbers), std::end (numbers), [](const std::pair<std::string, int >& entry) { std::cout << entry.first << " = " << entry.second << '\n' ; } ); }
1 2 3 4 5 std::for_each(std::begin (numbers), std::end (numbers), [](const auto & entry) { std::cout << entry.first << " = " << entry.second << '\n' ; } );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <algorithm> #include <iostream> #include <map> #include <string> int main () { const std::map<std::string, int > numbers { { "one" , 1 }, {"two" , 2 }, { "three" , 3 } }; for (auto mit = numbers.cbegin (); mit != numbers.cend (); ++mit) std::cout << &mit->first << ", " << &mit->second << '\n' ; std::for_each(std::begin (numbers), std::end (numbers), [](const std::pair<std::string, int >& entry) { std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n' ; } ); std::for_each(std::begin (numbers), std::end (numbers), [](const auto & entry) { std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n' ; } ); }
1 2 3 4 5 6 7 8 9 0x165dc40, 0x165dc60 0x165dce0, 0x165dd00 0x165dc90, 0x165dcb0 0x7ffe5ed29a20, 0x7ffe5ed29a40: one = 1 0x7ffe5ed29a20, 0x7ffe5ed29a40: three = 3 0x7ffe5ed29a20, 0x7ffe5ed29a40: two = 2 0x165dc40, 0x165dc60: one = 1 0x165dce0, 0x165dd00: three = 3 0x165dc90, 0x165dcb0: two = 2
使用 Lambdas 替代 std::bind1st 和 std::bind2nd 1 2 3 const auto onePlus = std::bind1st (std::plus <int >(), 1 );const auto minusOne = std::bind2nd (std::minus <int >(), 1 );std::cout << onePlus (10 ) << ", " << minusOne (10 ) << '\n' ;
使用现代 C++ 技术 1 2 3 4 5 6 7 8 9 10 11 #include <algorithm> #include <functional> #include <iostream> int main () { using std::placeholders::_1; const auto onePlus = std::bind (std::plus <int >(), _1, 1 ); const auto minusOne = std::bind (std::minus <int >(), 1 , _1); std::cout << onePlus (10 ) << ", " << minusOne (10 ) << '\n' ; }
函数组合 基于 Lambda 的 LIFT 技术 递归 Lambda 使用 std::function 内部 Lambda 与泛型参数 进阶技巧 递归 Lambda 是最佳选择吗? 总结 1 2 3 auto lamOnePlus1 = [](int b) { return 1 + b; };auto lamMinusOne1 = [](int b) { return b - 1 ; };std::cout << lamOnePlus1 (10 ) << ", " << lamMinusOne1 (10 ) << '\n' ;
1 2 3 auto lamOnePlus = [a=1 ](int b) { return a + b; };auto lamMinusOne = [a=1 ](int b) { return b - a; };std::cout << lamOnePlus (10 ) << ", " << lamMinusOne (10 ) << '\n' ;
Ex3_15:用 std::bind 组合函数。在线代码 @Wandbox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <algorithm> #include <functional> #include <vector> int main () { using std::placeholders::_1; const std::vector<int > v {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; const auto more2less6 = std::count_if (v.begin (), v.end (), std::bind (std::logical_and <bool >(), std::bind (std::greater <int >(), _1, 2 ), std::bind (std::less <int >(), _1, 6 ))); return more2less6; }
1 2 3 const std::vector<int > v {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 };const auto more2less6 = std::count_if (v.begin (), v.end (), [](int x) { return x > 2 && x < 6 ; });
3.4 泛型 Lambda 3.5 用 Lambda 代替 std::bind1st 和 std::bind2nd 3.6 Lambda 与 LIFT 惯用法 Calling function overloads
1 2 3 4 5 6 7 8 9 10 11 #include <algorithm> #include <vector> void foo (int ) {}void foo (float ) {}int main () { const std::vector<int > vi {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; std::for_each(vi.begin (), vi.end (), foo); }
1 2 3 4 5 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^
1 std::for_each(vi.begin (), vi.end (), [](auto x) { return foo (x); });
1 2 3 std::for_each(vi.begin (), vi.end (), [](auto && x) { return foo (std::forward<decltype (x)>(x)); });
1 2 3 const std::vector<int > v {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 };const auto more2less6 = std::count_if (v.begin (), v.end (), [](int x) { return x > 2 && x < 6 ; });
Ex3_16:泛型 lambda 与 函数重载。在线代码 @Wandbox
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <algorithm> #include <iostream> #include <vector> void foo (int i) { std::cout << "int: " << i << "\n" ; }void foo (float f) { std::cout << "float: " << f << "\n" ; }int main () { std::vector<int > vi {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; std::for_each(vi.begin (), vi.end (), [](auto && x) { return foo (std::forward<decltype (x)>(x)); }); }
1 2 3 4 5 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)> (x)...))) \ -> decltype(foo(std::forward<decltype(x)> (x)...)) \ { return foo(std::forward<decltype(x)> (x)...); }
3.7 递归 Lambda Ex3_17:普通函数递归。在线代码 @Wandbox
1 2 3 4 5 6 7 int factorial (int n) { return n > 1 ? n * factorial (n - 1 ) : 1 ; } int main () { return factorial (5 ); }
Ex3_18:Lambda 递归报错。在线代码 @Wandbox
1 2 3 4 5 6 int main () { auto factorial = [](int n) { return n > 1 ? n * factorial (n - 1 ) : 1 ; }; return factorial (5 ); }
1 error: use of 'factorial' before deduction of 'auto'
1 2 3 4 5 6 7 struct fact { int operator () (int n) const { return n > 1 ? n * factorial (n - 1 ) : 1 ; }; }; auto factorial = fact{};
Ex3_19:使用 std::function 的递归 lambda。在线代码 @Wandbox
1 2 3 4 5 6 7 8 #include <functional> int main () { const std::function<int (int )> factorial = [&factorial](int n) { return n > 1 ? n * factorial (n - 1 ) : 1 ; }; return factorial (5 ); }
Ex3_20:内部实现的递归 lambda。在线代码 @Wandbox
1 2 3 4 5 6 7 8 9 int main () { const auto factorial = [](int n) noexcept { const auto fact_impl = [](int n, const auto & impl) noexcept -> int { return n > 1 ? n * impl (n - 1 , impl) : 1 ; }; return fact_impl (n, fact_impl); }; return factorial (5 ); }
3.8 总结
[^7]: You can read more about universal references in this article from Scott Meyers: Universal References in C++11 [^8]: I used val as a vague name on purpose, so its meaning is not clear. [^9]: https://abseil.io/tips/108 [^10]: For more information and proposals on how to improve the syntax, you can read this blog post Passing overload sets to functions by Sy Brand. [^11]: https://wandbox.org/permlink/r81jASiPPmYXTOmx [^12]: We discussed assigning to std::function in the “The Type of a Lambda Expression” in the C++11 chapter. [^13]: https://stackoverflow.com/questions/2067988/recursive-lambda-functions-in-c11 [^14]: http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/