20-7 Lambda 捕获(todo)
捕获子句与按值捕获
在上节课(20.6——lambda表达式(匿名函数)介绍)中,我们介绍了这个示例:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
auto found{ std::find_if(arr.begin(), arr.end(),
[](std::string_view str)
{
return str.find("nut") != std::string_view::npos;
}) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
现在,让我们修改nut示例,让用户选择要搜索的子字符串。这并不像你想象的那样直观。
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// Ask the user what to search for.
std::cout << "search for: ";
std::string search{};
std::cin >> search;
auto found{ std::find_if(arr.begin(), arr.end(), [](std::string_view str) {
// Search for @search rather than "nut".
return str.find(search) != std::string_view::npos; // Error: search not accessible in this scope
}) };
if (found == arr.end())
{
std::cout << "Not found\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}

此代码无法编译。与嵌套代码块不同——外层代码块中可访问的任何标识符在嵌套代码块中均可访问——lambda表达式仅能访问在lambda外部定义的特定类型对象,包括:
- 具有静态(或线程局部)存储周期的对象(涵盖全局变量和静态局部变量)
- constexpr对象(显式或隐式声明)
由于 search 不满足上述任何条件,lambda 无法访问它。
提示:
lambda 只能访问在 lambda 外部定义的特定类型对象,包括具有静态存储期的对象(如全局变量和静态局部变量)以及 constexpr 对象。
要在 lambda 内部访问 search,我们需要使用捕获子句。
捕获子句
捕获子句capture clause用于(间接地)使lambda表达式能够访问其通常无法访问的外部作用域中的变量。我们只需在捕获子句中列出希望从lambda内部访问的实体即可。在此示例中,我们希望让lambda表达式能够访问变量search的值,因此将其添加到捕获子句中:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
std::cout << "search for: ";
std::string search{};
std::cin >> search;
// Capture @search vvvvvv
auto found{ std::find_if(arr.begin(), arr.end(), [search](std::string_view str) {
return str.find(search) != std::string_view::npos;
}) };
if (found == arr.end())
{
std::cout << "Not found\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
用户现在可以搜索我们数组中的某个元素。
输出

那么捕获机制究竟如何运作?
虽然上例中的lambda表达式看似直接访问了main函数中search变量的值,但实际并非如此。lambda表达式看似嵌套代码块,但其工作原理略有不同(且这种区别至关重要)。
当lambda定义被执行时,对于每个被捕获的变量,都会在lambda内部创建一个同名变量的克隆副本。这些克隆变量此时会从外部作用域中同名变量初始化。
因此在上例中,当lambda对象创建时,它会获得名为search的独立克隆变量。该克隆变量与main中的search值相同,故表现上看似访问了main的search,实则不然。
虽然这些克隆变量名称相同,但类型未必与原始变量一致。本节后续内容将深入探讨此特性。
关键要点:
lambda捕获的变量是外部作用域变量的副本,而非原始变量本身。
进阶知识:
尽管lambda看似函数,其实是可像函数调用的对象(称为函子functors——后续课程将讲解如何从零创建函子)。编译器遇到lambda定义时,会为其创建专属对象定义。每个捕获变量都成为该对象的数据成员。
运行时遇到lambda定义时,会实例化lambda对象,并在此阶段初始化其成员。
默认情况下,捕获变量被视为 const
当调用 lambda 表达式时,会调用 operator()。默认情况下,此 operator() 将捕获变量视为 const,这意味着 lambda 表达式不允许修改这些捕获变量。
在下面的示例中,我们捕获变量 ammo 并尝试对其进行递减操作。
#include <iostream>
int main()
{
int ammo{ 10 };
// Define a lambda and store it in a variable called "shoot".
auto shoot{
[ammo]() {
// Illegal, ammo cannot be modified.
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
// Call the lambda
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}

上述代码无法编译,因为在lambda表达式内部,ammo被视为const类型。
可变捕获
为允许修改捕获的变量,我们可以将lambda标记为可变:
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
[ammo]() mutable { // now mutable
// We're allowed to modify ammo now
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
输出:

虽然现在能编译通过,但逻辑错误依然存在。问题出在哪里?当lambda被调用时,它捕获了ammo的副本。当lambda将ammo从10递减到9再到8时,它递减的是自己的副本,而非main()中的原始ammo值。
请注意:ammo的值在多次调用lambda时始终保持不变!
警告:
由于捕获的变量是lambda对象的成员,其值会在多次调用lambda时持续存在!
按引用捕获
正如函数可以修改按引用传递的参数值,我们也可以通过按引用捕获变量,使lambda表达式能够影响参数的值。
要按引用捕获变量,需在捕获语句中于变量名前添加一个&符号。与按值捕获的变量不同,按引用捕获的变量默认是非const的,除非被捕获的变量本身是const。当你通常更倾向于按引用传递函数参数时(例如对非基本类型),应优先选择按引用捕获而非按值捕获。
以下是将ammo按引用捕获的示例代码:
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
// We don't need mutable anymore
[&ammo]() { // &ammo means ammo is captured by reference
// Changes to ammo will affect main's ammo
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
这产生了预期的答案:

现在,让我们使用引用捕获来统计 std::sort 在排序数组时执行了多少次比较操作。
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct Car
{
std::string_view make{};
std::string_view model{};
};
int main()
{
std::array<Car, 3> cars{ { { "Volkswagen", "Golf" },
{ "Toyota", "Corolla" },
{ "Honda", "Civic" } } };
int comparisons{ 0 };
std::sort(cars.begin(), cars.end(),
// Capture @comparisons by reference.
[&comparisons](const auto& a, const auto& b) {
// We captured comparisons by reference. We can modify it without "mutable".
++comparisons;
// Sort the cars by their make.
return a.make < b.make;
});
std::cout << "Comparisons: " << comparisons << '\n';
for (const auto& car : cars)
{
std::cout << car.make << ' ' << car.model << '\n';
}
return 0;
}
可能的输出

捕获多个变量
通过用逗号分隔多个变量即可捕获它们。这可以包含按值捕获和按引用捕获的混合变量:
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// Capture health and armor by value, and enemies by reference.
[health, armor, &enemies](){};
默认捕获
显式列出需要捕获的变量可能很繁琐。修改lambda表达式时,可能会忘记添加或移除捕获的变量。幸运的是,我们可以借助编译器自动生成需要捕获的变量列表。
默认捕获default capture(也称为捕获默认值capture-default)会捕获lambda中提及的所有变量。若使用默认捕获,lambda中未提及的变量则不会被捕获。
要按值捕获所有使用的变量,请使用捕获值 =。
要按引用捕获所有使用的变量,请使用捕获值 &。
以下是按值使用默认捕获的示例:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array areas{ 100, 25, 121, 40, 56 };
int width{};
int height{};
std::cout << "Enter width and height: ";
std::cin >> width >> height;
auto found{ std::find_if(areas.begin(), areas.end(),
[=](int knownArea) { // will default capture width and height by value
return width * height == knownArea; // because they're mentioned here
}) };
if (found == areas.end())
{
std::cout << "I don't know this area :(\n";
}
else
{
std::cout << "Area found :)\n";
}
return 0;
}

默认捕获可以与常规捕获混合使用。我们可以按值捕获某些变量,按引用捕获其他变量,但每个变量只能被捕获一次。
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// Capture health and armor by value, and enemies by reference.
[health, armor, &enemies](){};
// Capture enemies by reference and everything else by value.
[=, &enemies](){};
// Capture armor by value and everything else by reference.
[&, armor](){};
// Illegal, we already said we want to capture everything by reference.
[&, &armor](){};
// Illegal, we already said we want to capture everything by value.
[=, armor](){};
// Illegal, armor appears twice.
[armor, &health, &armor](){};
// Illegal, the default capture has to be the first element in the capture group.
[armor, &](){};
在lambda捕获中定义新变量
有时我们希望捕获一个略有修改的变量,或声明一个仅在lambda作用域内可见的新变量。我们可以通过在lambda捕获中定义变量而不指定其类型来实现。
#include <array>
#include <iostream>
#include <algorithm>
int main()
{
std::array areas{ 100, 25, 121, 40, 56 };
int width{};
int height{};
std::cout << "Enter width and height: ";
std::cin >> width >> height;
// We store areas, but the user entered width and height.
// We need to calculate the area before we can search for it.
auto found{ std::find_if(areas.begin(), areas.end(),
// Declare a new variable that's visible only to the lambda.
// The type of userArea is automatically deduced to int.
[userArea{ width * height }](int knownArea) {
return userArea == knownArea;
}) };
if (found == areas.end())
{
std::cout << "I don't know this area :(\n";
}
else
{
std::cout << "Area found :)\n";
}
return 0;
}

userArea 仅在 lambda 定义时计算一次。计算结果存储在 lambda 对象中,每次调用时保持不变。若 lambda 为可变类型且修改了捕获范围内的变量,则原始值将被覆盖。
最佳实践:
仅当变量值简短且类型明确时,才应在捕获范围内初始化变量。否则,建议在 lambda 外部定义变量并进行捕获。
悬空捕获变量
变量在lambda表达式定义处被捕获。若通过引用捕获的变量在lambda表达式终止前消失,该lambda将持有悬空引用。
例如:
#include <iostream>
#include <string>
// returns a lambda
auto makeWalrus(const std::string& name)
{
// Capture name by reference and return the lambda.
return [&]() {
std::cout << "I am a walrus, my name is " << name << '\n'; // Undefined behavior
};
}
int main()
{
// Create a new walrus whose name is Roofus.
// sayName is the lambda returned by makeWalrus.
auto sayName{ makeWalrus("Roofus") };
// Call the lambda function that makeWalrus returned.
sayName();
return 0;
}

对 makeWalrus() 的调用会从字符串字面量 “Roofus” 创建一个临时 std::string。makeWalrus() 中的 lambda 表达式通过引用捕获该临时字符串。当包含 makeWalrus() 调用的完整表达式结束时,临时字符串会销毁,但 lambda 表达式 sayName 仍会在此之后继续引用它。因此,当调用 sayName 时,会访问悬空引用,导致未定义行为。
需注意,即使将“Roofus”按值传递给makeWalrus()也会发生此问题。参数name在makeWalrus()结束时销毁,而lambda仍持有悬空引用。
警告:
通过引用捕获变量时需格外谨慎,尤其采用默认引用捕获时。被捕获的变量必须比lambda表达式存活更久。
若需确保捕获的名称在使用lambda时有效,必须改用值捕获(通过显式捕获或默认值捕获)。
可变lambda表达式的意外复制
由于lambda表达式是对象,它们可以被复制。在某些情况下,这可能会引发问题。请考虑以下代码:
#include <iostream>
int main()
{
int i{ 0 };
// Create a new lambda named count
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
count(); // invoke count
auto otherCount{ count }; // create a copy of count
// invoke both count and the copy
count();
otherCount();
return 0;
}
输出

代码并未打印1、2、3,而是重复打印了两次2。当我们将otherCount定义为count的副本时,创建的是count当前状态的副本。此时count的i值为1,因此otherCount的i值同样为1。由于otherCount是count的副本,它们各自拥有独立的i变量。
现在来看一个稍显隐晦的例子:
#include <iostream>
#include <functional>
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// Increments and prints its local copy of @i.
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
myInvoke(count);
myInvoke(count);
myInvoke(count);
return 0;
}
输出:

这与前例存在相同问题,只是表现形式更为隐晦。
当调用 myInvoke(count) 时,编译器会发现 count(具有 lambda 类型)与引用参数类型(std::function<void()>)不匹配。编译器会将lambda转换为临时std::function对象,以便引用参数能与其绑定,此过程会生成lambda的副本。因此,我们对fn()的调用实际是在临时std::function对象中存在的lambda副本上执行的,而非原始lambda本身。
若需传递可变lambda且希望避免意外复制,有两种解决方案:其一是改用非捕获lambda——如前例中可移除捕获机制,转而使用静态局部变量维护状态。但静态局部变量难以追踪且降低代码可读性。更优方案是从源头阻止lambda被复制。然而我们无法干预std::function(或其他标准库函数/对象)的实现机制,该如何实现呢?
一种方案(感谢读者Dck的建议)是将lambda立即封装到std::function中。这样在调用myInvoke()时,引用参数fn可直接绑定到std::function实例,从而避免临时副本的生成:
#include <iostream>
#include <functional>
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// Increments and prints its local copy of @i.
std::function count{ [i]() mutable { // lambda object stored in a std::function
std::cout << ++i << '\n';
} };
myInvoke(count); // doesn't create copy when called
myInvoke(count); // doesn't create copy when called
myInvoke(count); // doesn't create copy when called
return 0;
}
我们的输出结果现在符合预期:

另一种解决方案是使用引用包装器。C++提供了一个便捷的类型(作为
以下是使用 std::ref 后的更新代码:
#include <iostream>
#include <functional> // includes std::reference_wrapper and std::ref
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// Increments and prints its local copy of @i.
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
// std::ref(count) ensures count is treated like a reference
// thus, anything that tries to copy count will actually copy the reference
// ensuring that only one count exists
myInvoke(std::ref(count));
myInvoke(std::ref(count));
myInvoke(std::ref(count));
return 0;
}
我们的输出结果现在符合预期:

此方法的有趣之处在于,即使 myInvoke 以值传递方式(而非引用传递)接收 fn,它依然有效!
规则:
标准库函数可能复制函数对象(提醒:lambda 是函数对象)。若需提供包含可变捕获变量的 lambda,请使用 std::ref 以引用传递方式传递。
最佳实践:
尽量避免使用可变lambda。不可变lambda更易于理解,既能规避上述问题,又能避免在并行执行场景中引发的更危险问题。
测验时间
问题 #1
以下哪个变量可以在 main 中的 lambda 中使用而无需显式捕获?
int i{};
static int j{};
int getValue()
{
return 0;
}
int main()
{
int a{};
constexpr int b{};
static int c{};
static constexpr int d{};
const int e{};
const int f{ getValue() };
static const int g{};
static const int h{ getValue() };
[](){
// Try to use the variables without explicitly capturing them.
a;
b;
c;
d;
e;
f;
g;
h;
i;
j;
}();
return 0;
}
显示解决方案:
| Variable | Usable without explicit capture |
|---|---|
| a | No. a has automatic storage duration. |
| b | Yes. b is usable in a constant expression. |
| c | Yes. c has static storage duration. |
| d | Yes. |
| e | Yes. e is usable in a constant expression. |
| f | No. f‘s value depends on getValue, which might require the program to run. |
| g | Yes. |
| h | Yes. h has static storage duration. |
| i | Yes. i is a global variable. |
| j | Yes. j is accessible in the entire file. |
问题 #2
以下代码会输出什么?不要运行代码,请在脑海中推导答案。
#include <iostream>
#include <string>
int main()
{
std::string favoriteFruit{ "grapes" };
auto printFavoriteFruit{
[=]() {
std::cout << "I like " << favoriteFruit << '\n';
}
};
favoriteFruit = "bananas with chocolate";
printFavoriteFruit();
return 0;
}
显示解决方案
I like grapesprintFavoriteFruit 通过值捕获 favoriteFruit。修改 main 的 favoriteFruit 不会影响 lambda 的 favoriteFruit。
问题 #3
我们将编写一个基于平方数的简单游戏(平方数指通过整数自乘得到的数,如 1, 4, 9, 16, 25, …)。
游戏设置步骤:
- 询问用户输入起始数(例如3)。
- 询问用户需要生成多少个数值。
- 随机选取2至4之间的整数作为乘数。
- 根据用户指定的数量生成数值序列。从起始数开始,每个数值应为下一个平方数乘以乘数。
游戏流程:
- 用户输入猜测值。
- 若猜测值与生成的数值匹配,则该数值从列表中移除,用户可继续猜测。
- 若用户猜中所有生成的数值,则获胜。
- 若猜测值与生成的数值不匹配,用户失败,程序将告知最接近的未猜中数值。
以下示例会话可帮助您更好地理解游戏机制:
Start where? 4
How many? 5
I generated 5 square numbers. Do you know what each number is after multiplying it by 2?
> 32
Nice! 4 number(s) left.
> 72
Nice! 3 number(s) left.
> 50
Nice! 2 number(s) left.
> 126
126 is wrong! Try 128 next time.
- 程序从4开始生成接下来的5个平方数:16, 25, 36, 49, 64
- 程序随机选取2作为乘数,因此每个平方数乘以2:32, 50, 72, 98, 128
- 现在轮到用户猜测。
- 32在列表中。
- 72在列表中。
- 126不在列表中,因此用户猜错。最接近的未猜中数字是128。
Start where? 1
How many? 3
I generated 3 square numbers. Do you know what each number is after multiplying it by 4?
> 4
Nice! 2 number(s) left.
> 16
Nice! 1 number(s) left.
> 36
Nice! You found all numbers, good job!
程序从1开始生成接下来的3个平方数:1, 4, 9
程序随机选取4作为乘数,因此每个平方数乘以4:4, 16, 36
用户正确猜出所有数字即获胜。
提示:
- 使用Random.h(8.15节——全局随机数生成(Random.h))生成随机数。
- 使用std::find()(18.3节——标准库算法介绍)在列表中搜索数字。
- 使用std::vector::erase()删除元素,例如:
auto found{ std::find(/* ... */) }; // Make sure the element was found myVector.erase(found);
- 使用 std::min_element 和 lambda 表达式找出最接近用户猜测的数字。std::min_element 的工作原理与前次测验中的 std::max_element 类似。
显示提示
提示:使用<cmath>中的std::abs函数计算两个数之间的正差值。
int distance{ std::abs(3 - 5) }; // 2
显示解决方案
#include <algorithm> // std::find, std::min_element
#include <cmath> // std::abs
#include <cstddef> // std::size_t
#include <iostream>
#include <vector>
#include "Random.h"
using Numbers = std::vector<int>;
namespace config
{
constexpr int multiplierMin{ 2 };
constexpr int multiplierMax{ 6 };
}
// Generates @count numbers starting at @start*@start and multiplies
// every square number by @multiplier.
Numbers generateNumbers(int start, int count, int multiplier)
{
Numbers numbers(static_cast<std::size_t>(count));
for (int index = 0; index < count; ++index)
{
std::size_t uindex{ static_cast<std::size_t>(index) };
numbers[uindex] = (start + index) * (start + index) * multiplier;
}
return numbers;
}
// Asks the user to input starting number, then generates array of numbers
Numbers setupGame()
{
int start{};
std::cout << "Start where? ";
std::cin >> start;
int count{};
std::cout << "How many? ";
std::cin >> count;
int multiplier{ Random::get(config::multiplierMin, config::multiplierMax) };
std::cout << "I generated " << count
<< " square numbers. Do you know what each number is after multiplying it by "
<< multiplier << "?\n";
return generateNumbers(start, count, multiplier);
}
// Returns the user's guess
int getUserGuess()
{
int guess{};
std::cout << "> ";
std::cin >> guess;
return guess;
}
// Searches for the value @guess in @numbers and removes it.
// Returns true if the value was found. False otherwise.
bool findAndRemove(Numbers& numbers, int guess)
{
auto found{ std::find(numbers.begin(), numbers.end(), guess) };
if (found == numbers.end())
return false;
numbers.erase(found);
return true;
}
// Finds the value in @numbers that is closest to @guess.
int findClosestNumber(const Numbers& numbers, int guess)
{
return *std::min_element(numbers.begin(), numbers.end(),
[=](int a, int b)
{
return std::abs(a - guess) < std::abs(b - guess);
});
}
// Called when the user guesses a number correctly.
void printSuccess(const Numbers& numbers)
{
std::cout << "Nice! ";
if (numbers.size() == 0)
{
std::cout << "You found all numbers, good job!\n";
}
else
{
std::cout << numbers.size() << " number(s) left.\n";
}
}
// Called when the user guesses a number that is not in the numbers.
void printFailure(const Numbers& numbers, int guess)
{
int closest{ findClosestNumber(numbers, guess) };
std::cout << guess << " is wrong!\n";
std::cout << "Try " << closest << " next time.\n";
}
int main()
{
Numbers numbers{ setupGame() };
while (true)
{
int guess{ getUserGuess() };
if (!findAndRemove(numbers, guess))
{
printFailure(numbers, guess);
break;
}
printSuccess(numbers);
if (numbers.size() == 0)
break;
}
return 0;
}

浙公网安备 33010602011771号