C++14 中的 Lambda

C++14 对 Lambda 表达式增加了两个重要的功能:

  • 带初始化器的捕获
  • 泛型 Lambda

此外,该标准还更新了一些规则,例如:

  • Lambda 的默认参数
  • auto 作为返回类型

这些特性可以解决 C++11 中出现的几个问题。你可以在 N4140 和 Lambdas 中查看相关细节。此外,在本章,你将会了解到:

  • 捕获非静态数据成员
  • 用现代的技术替代旧的函数风格工具,如 std::bind1st
  • LIFTING 惯用法
  • 递归 Lambda

Lambda 的默认参数

让我们从一些较小的更新开始:

在 C++14 中,可以在函数调用中使用默认参数。这是一个小功能,但可以使 Lambda 更像一个常规函数。

1
2
3
4
5
6
7
8
// Ex3_1: Lambda with Default Parameter.
#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; // float!
return x * 2.1;// double!
};

上述代码无法通过编译:第一个 return 语句返回 float 类型,而第二个返回 double 类型。编译器无法自行裁决,必须由开发者明确指定单一返回类型。

虽然数值类型(如整型/浮点型)的自动推导有其便利性,但返回类型推导的真正价值体现在更重要的场景中。该特性在模板编程和处理”未知类型”时具有关键作用。

例如,Lambda 闭包类型本身是匿名类型,我们无法在代码中显式声明。若需要从函数返回一个 Lambda,该如何指定返回类型?在 C++14 之前,只能通过 std::function 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Ex3_2: Returning std::function.
#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
// Ex3_4: Capture With an Initialiser. 
#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 后修改 xy 的值,变量 z 不会改变。在示例中可以看到,即便在定义 Lambda 后立即修改了 xy 的值,输出仍然是 42,因为 z 早已完成初始化。

通过初始化器创建变量非常灵活,例如你还可以创建对外部作用域变量的引用。

1
2
3
4
5
6
7
8
9
10
// Ex3_5: Reference as Capture With an Initialiser.
#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
// Ex3_6: Capturing a movable only type.
#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
// Ex3_7: std::function and std::move.
std::unique_ptr<int> p(new int{10});
std::function<void()> fn = [ptr = std::move(p)]() { }; // won't compile!
优化
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
// Ex3_9: Capturing a data member.
#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
// Ex3_10: Generic Variadic Lambda, Sum.
#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
// Ex3_11: Perfect Forwarding with Generic Lambda.
#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
// Ex3_12: Correct type for map iteration.
#include <algorithm>
#include <iotream>
#include <map>
#include <string>

int main() {
const std::map<std::string, int> numbers {
{ "one", 1 }, { "two", 2 }, { "three", 3 }
};

// each time entry is copied from pair<const string, int>!
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
// Ex3_13: Correct type for map iteration, full version.
#include <algorithm>
#include <iostream>
#include <map>
#include <string>

int main() {
const std::map<std::string, int> numbers {
{ "one", 1 }, {"two", 2 }, { "three", 3 }
};

// print addresses:
for (auto mit = numbers.cbegin(); mit != numbers.cend(); ++mit)
std::cout << &mit->first << ", " << &mit->second << '\n';

// each time entry is copied from pair<const string, int>!
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';
}
);

// this time entries are not copied, they have the same addresses
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::bind1ststd::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
// Ex3_14: Replacing with std::bind.
#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>

// two overloads:
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/