从零开始 / 一次只解释眼前的一步
共享拥有与
回调寿命。
先看最后一个强拥有者何时离开,再区分弱观察与成功lock取得的强拥有。把拥有关系带入回调,检查回调离开创建位置之后还能安全使用什么。
6小时是包含阅读、推演和编码的设计估算,未经真人试学校准。可分多次学习,遇到不清楚的地方保留预测、实际结果和疑问,之后再回修教材。手机可读图与做预测,编译需要电脑终端。
这一章怎样学
先读一小段,写下预测,再运行程序。每次只改一个条件,最后关掉示例,从空文件独立写一次。遇到错误,把第一条报错和自己的修复记下来;不用赶着把页面滚到底。
每次写下强拥有者、弱观察者和回调保存的状态,再检查对象是否仍活着、调用目标是否有效。先完成第13章真实资源与RAII,再开始本章;此前的M1仍按自己的独立材料核对,打开本页不会替你判定M1或G0通过。
14.1 shared_ptr:最后一份强拥有关系结束时才销毁
先完成13章的真实资源与RAII。上一章的unique_ptr只有一份独占责任,转交以后来源变空。如果两个位置都必须独立保证同一个对象存活,该怎样安排结束时点?本章先观察这种共享关系,再把“谁保活”带入可能晚些才调用的函数对象。
复制的是拥有关系,不是所管对象
<memory>提供std::shared_ptr<T>和std::make_shared<T>(参数)。本章只用它们管理普通单个对象:std::make_shared<Trace>(7)创建一个Trace7,返回它的强拥有者。随后auto second = first;复制shared_ptr,second与first共享同一个Trace。没有第二个Trace构造,也没有把7复制到另一个资源对象里。
库会维护这一组拥有者共用的控制关系,通常称为控制块(control block)。它记录还有多少份强拥有关系,并安排最终销毁。这里画的是逻辑关系,不要求预测控制块的物理地址、大小或分配次数。直接复制或赋值已有shared_ptr会延续该关系;不要拿同一裸地址分别建立互不知情的拥有者,那会重复安排释放。
对本章非空拥有者,owner->value和*owner访问所管对象,仍须遵守对象存活的前提。std::shared_ptr<Trace> empty{};默认构造为空;不写初始化式时也默认构造为空。在if中使用owner表示非空,!owner表示空;它们检查有没有对象,与对象中存的是7还是0无关。此处沿用13章的条件转换读法,只把它应用到shared_ptr的对应合同。
reset放弃自己这一份;use_count只观察数量
first.reset()使first为空,结束first这一份强拥有关系。若还有second,Trace继续存活;若刚好是最后一份,本章的默认销毁方式就结束Trace寿命。非空shared_ptr正常析构时也放弃自己的那一份。空拥有者析构则没有所管对象需要释放。
first.use_count()返回与first共享该控制关系的强拥有者数量;空拥有者得到0。调用本身不复制拥有者,也不改变数量。下面只在确定的单线程、明确的作用域中读这个数,用来对照源码。不能从“此刻数量是1”推出另一段并发代码可以无同步地修改对象;线程安全是后续系统章节的问题。
#include <iostream>
#include <memory>
struct Trace {
int value;
explicit Trace(int initial) : value{initial} {}
~Trace() {
std::cout << "destroy " << value << '\n';
}
};
int main() {
auto first = std::make_shared<Trace>(7);
{
auto second = first;
std::cout << "owners " << first.use_count() << '\n';
first.reset();
std::cout << "remaining " << second->value << ' ' << second.use_count() << '\n';
}
std::cout << "done\n";
return 0;
}
| 走到哪里 | Trace7的强拥有者 | 输出与原因 |
|---|---|---|
| second复制first之后 | first、second | owners 2:两个管理对象共享同一份Trace |
| first.reset之后 | second | remaining 7 1:还能读7,数量现在为1 |
| 内层作用域结束 | 没有 | second析构放弃最后一份,Trace析构打印destroy 7 |
| 回到外层 | first为空 | done:first仍存在,但没有对象可释放 |
先只把初值7换成0,重新预测四行。数量与时点不变,remaining和destroy中的数值变0。这个变化再次区分“空拥有者”和“拥有值为0的对象”。
共享并非越多越好:如果同步调用只需暂时读对象,13章的引用借用已经能表达。只有确实需要独立延长对象寿命的位置,才增加拥有关系。下一节引入一种能检查对象是否还在、却不负责延长其寿命的观察关系。
14.2 weak_ptr:观察不会保活,成功lock会取得强拥有
std::weak_ptr<T>保存弱观察关系。它可从同型shared_ptr初始化,也可通过watch = first;改为观察first所关联的对象;这些操作不增加强拥有者数量。std::weak_ptr<Trace> watch{};默认没有关联。它不提供直接访问所管Trace的解引用操作;不能把watch当作一个无需检查的Trace指针。
观察状态和取得访问资格是两件事
watch.expired()检查关联对象是否已没有强拥有者;从未关联的默认weak_ptr也返回true。这个查询不取得拥有关系。watch.lock()则尝试返回一份shared_ptr:对象仍可保活时取得强拥有关系,否则得到空shared_ptr。成功结果存进held以后,held在自己的寿命内保活对象,直到它放弃拥有;watch本身仍然是弱观察。
因此需要访问时,直接lock并判断结果。不要把一次expired()==false当成已经拿到强拥有者,随后另走一条没有保活保证的裸地址。查询的答案和保活的责任不是同一件东西。
在if前半部分命名结果
这里新增一种if读法:if (auto held = watch.lock(); held)。括号内分号之前是初始化语句,先执行一次并建立held;分号后才是条件,按shared_ptr的条件转换检查held是否非空。held的作用域覆盖整个if,包括else分支;整个if语句结束时held销毁。它不是while循环,也不会反复lock。
下面两个if中的held是两个不同作用域的局部名字。第一个if没有else,结束后第一份held就已销毁;最后另一个if重新调用lock,不能沿用前一次成功的结果。
#include <iostream>
#include <memory>
struct Trace {
int value;
explicit Trace(int initial) : value{initial} {}
~Trace() {
std::cout << "destroy " << value << '\n';
}
};
int main() {
std::weak_ptr<Trace> watch{};
{
auto first = std::make_shared<Trace>(7);
auto second = first;
watch = first;
std::cout << "owners " << first.use_count() << '\n';
first.reset();
std::cout << "remaining " << second->value << '\n';
if (auto held = watch.lock(); held) {
std::cout << "locked " << held->value << '\n';
}
second.reset();
}
std::cout << "expired " << watch.expired() << '\n';
if (auto held = watch.lock(); held) {
std::cout << "locked " << held->value << '\n';
} else {
std::cout << "empty\n";
}
return 0;
}
完整例一先建立first与second,watch接入后仍打印owners 2。first.reset只留下second,输出remaining 7。第一次lock成功,分支中held也成为强拥有者,所以能输出locked 7。先结束if,释放held,再执行second.reset:这时才没有强拥有者,输出destroy 7。
外层watch仍在,因此可以查询过期状态,输出expired 1。第二次lock得到空shared_ptr,走else,输出empty。对象已经销毁;weak_ptr不会让它复活。六行依次为owners 2、remaining 7、locked 7、destroy 7、expired 1、empty。
双向连接不必双向拥有
设一个父节点需要保活子节点,子节点只需要能找到仍存在的父节点。用父的shared_ptr成员保存子、子的weak_ptr成员观察父,就能表达这两种不同责任。
下面Node定义内部的std::shared_ptr<Node>与std::weak_ptr<Node>使用当前类型自己的名字。成员里放的是智能指针对象,不是又直接嵌套一整份Node,因此不会形成“Node里有完整Node,里面再有完整Node”的无限结构。构造函数只显式初始化value;这两个类类型成员仍会默认构造为空。新07章的成员与构造读法继续适用。
parent->child = child;把局部child的强拥有关系复制进父节点的child成员;child->parent = parent;则设置弱回边,不增加父的强拥有数量。链式parent->child->value先访问父里的child成员,再沿那个非空shared_ptr访问子节点的value。它没有额外复制节点或拥有者。
#include <iostream>
#include <memory>
struct Node {
int value;
std::shared_ptr<Node> child;
std::weak_ptr<Node> parent;
explicit Node(int initial) : value{initial} {}
~Node() {
std::cout << "destroy " << value << '\n';
}
};
int main() {
std::weak_ptr<Node> parent_watch{};
std::weak_ptr<Node> child_watch{};
{
auto parent = std::make_shared<Node>(1);
auto child = std::make_shared<Node>(2);
parent->child = child;
child->parent = parent;
parent_watch = parent;
child_watch = child;
std::cout << "linked " << parent->value << ' ' << parent->child->value << '\n';
}
std::cout << "expired " << parent_watch.expired() << ' ' << child_watch.expired() << '\n';
return 0;
}
先打印linked 1 2。离开内层块时,按局部构造的逆序,先结束局部child;子节点仍被父的child成员拥有,因此还不销毁。随后局部parent结束,父没有其他强拥有者,父析构函数体打印destroy 1。函数体之后,其成员继续销毁;child成员放弃最后一份子拥有关系,子析构打印destroy 2。外部两个watch只弱观察,最终都过期,输出expired 1 1。
常见错误是把Node的parent成员也改成shared_ptr。外部局部名字退出后,父仍被子成员强持有,子仍被父成员强持有;两个对象互相保活,形成强拥有环,不会自动归零。这一派生版本只分析和编译,不执行。修复应回到实际责任:本例子不负责保活父,所以恢复weak_ptr;以后访问父时先lock并判断,而不是改成一个可能悬空的裸地址。
14.3 初始化捕获:把晚些要用的状态交给回调
10.4的lambda与捕获已经说明:创建闭包与调用闭包是两个时刻;按值捕获保存状态,引用捕获借用外部对象;普通函数可以用auto返回具体闭包。本节继续使用这些读法,新增“为捕获状态单独写初始化式”。
给内部状态一个自己的名字
[scale = factor]在创建闭包时,用外部factor初始化闭包自己的scale。scale的类型按auto推导;此处factor是int,所以保存一份int。lambda函数体里的scale表示这份捕获状态,外部不需要预先声明一个scale变量。此后修改外部factor不会改变已经保存的scale。
#include <iostream>
int main() {
int factor{4};
auto multiply = [scale = factor](int input) {
return input * scale;
};
factor = 0;
std::cout << factor << '\n';
std::cout << multiply(6) << '\n';
return 0;
}
创建multiply时factor是4,scale保存4;之后factor改为0,第一行输出0。调用multiply(6)读取内部scale,计算6×4,第二行输出24。保存状态不是保存“将来重新读取factor的公式”。如果需要变化同步,必须另行安排有效借用;不能只因变量名相似就推断自动同步。
初始化式也可以明确移动拥有者
[state = std::move(owner)]把owner提供的独占拥有关系移动初始化到闭包内部的state。沿13章合同,owner随后为空,state拥有原来那个对象。它不是复制unique_ptr,也不是只抄一个裸地址。闭包结束时,其state成员销毁,负责释放仍然拥有的对象。
下面工厂的auto返回类型沿用10.4,不要求你书写闭包的编译器生成类型。工厂只有一条return;main的auto接住具体闭包。返回过程中的具体复制省略次数不是本例预测目标,关键是返回结果实际持有独占状态。
#include <iostream>
#include <memory>
#include <utility>
auto make_multiplier(int factor) {
auto owner = std::make_unique<int>(factor);
return [state = std::move(owner)](int input) {
return input * (*state);
};
}
int main() {
auto times_four = make_multiplier(4);
auto times_zero = make_multiplier(0);
std::cout << times_four(6) << '\n';
std::cout << times_zero(6) << '\n';
return 0;
}
完整例二第一次调用make_multiplier(4):局部owner申请int4,return里的初始化捕获将责任移入state。工厂结束以后,局部owner这个名字已经消失,但times_four的状态仍拥有那个真实int。调用times_four(6)就能读取4,输出24。第二次工厂建立另一份int0,由times_zero拥有,调用得到0。
*state读取的是仍有效的倍率对象;这里只读、不修改unique_ptr本身,所以不需要mutable。0是一个实际倍率,不是空拥有者。两个闭包的状态彼此独立,各自在结束时释放自己的int。这里使用固定的小整数,不能由这两个结果推出任意输入相乘都不会溢出。
工厂返回后,由闭包继续拥有倍率
第一次make_multiplier调用建立真实int 4,暂由局部owner独占管理;回调还未形成。
state=std::move(owner)在闭包内初始化独占状态。owner变空,state接管同一个int,没有借用局部owner这个变量。
工厂局部名字已经结束,main中的times_four仍通过其捕获状态拥有int 4。源码不要求猜测返回过程的复制省略次数。
main又调用工厂建立times_zero,独占另一份值为0的int。两个闭包分别拥有自己的倍率;0不表示状态为空。
输入均为6,两个闭包依次输出24、0。之后闭包结束时,其unique_ptr状态分别负责释放各自的int。
查看所有步骤的文字与数值
- 1 · 工厂建立倍率4
工厂参数:factor=4;局部owner:拥有int 4;闭包:尚未形成
第一次make_multiplier调用建立真实int 4,暂由局部owner独占管理;回调还未形成。
- 2 · 初始化捕获接管
局部owner:空;闭包state:独占int 4;倍率对象:4,仍存活
state=std::move(owner)在闭包内初始化独占状态。owner变空,state接管同一个int,没有借用局部owner这个变量。
- 3 · 返回具体闭包
工厂局部:已结束;times_four状态:拥有int 4;可调用前提:状态对象仍存活
工厂局部名字已经结束,main中的times_four仍通过其捕获状态拥有int 4。源码不要求猜测返回过程的复制省略次数。
- 4 · 另一个闭包拥有0
times_four:倍率4;times_zero:倍率0;两份对象:独立且都存活
main又调用工厂建立times_zero,独占另一份值为0的int。两个闭包分别拥有自己的倍率;0不表示状态为空。
- 5 · 调用读取各自倍率
times_four(6):24;times_zero(6):0;最终释放者:各闭包内的state
输入均为6,两个闭包依次输出24、0。之后闭包结束时,其unique_ptr状态分别负责释放各自的int。
常见错误:回调借用了已经结束的局部变量
设把工厂的捕获改为[&owner],同时把函数体读取改为*owner。这是一项错误设计的两处源码编辑,不是“只改一个条件”的练习。闭包现在借用工厂局部owner;工厂退出时owner及其独占整数都会结束。之后调用返回的闭包要访问已经失效的局部对象,存在悬空借用,不能运行来猜一个数值答案。
编译器可能诊断这种逃出作用域的引用,也可能接受某种类似写法;本章只保存实际编译诊断,不预先承诺危险程序一定编译成功。正确版本使用上面的移动捕获,让回调自己拥有将来调用需要的数据。不是所有回调都需要动态分配:这里故意使用真实独占资源,来检验拥有关系有没有跨过工厂出口。
14.4 std::function:统一调用签名,还有目标的复制要求
到这里一直用auto保留具体闭包。如果一个变量需要先留空,之后接收满足同一调用合同的不同目标,可以使用<functional>中的std::function。它是标准库的可调用包装器;我们只教本节需要的签名、空状态、复制与更换目标,不展开它的内部存储和性能。
先读签名,再判断有没有目标
std::function<int(int)>中的外层尖括号沿用模板实参读法;里面的int(int)是函数类型,表示接收一个int并返回int。左侧int是结果类型,括号内int是参数类型,不是名为int的函数调用。存入本章闭包以后,operation(6)通过包装器调用所存目标并取得结果。
std::function<int(int)> operation{};默认没有可调用目标。if中的operation表示有目标,!operation表示空;这些检查不调用目标。空包装不能当作“返回0的函数”来调用。实际空调用的异常行为留在错误处理章接入,本节先判空,避免执行该路径。
复制包装保存目标副本;赋值可以更换当前目标
把满足签名且可复制的lambda赋给operation,会存入这个调用目标,替换operation此前的目标。auto copied = operation;则建立一个新包装,复制当时所存的可调用对象。本例目标只按值捕获int,所以两个包装各自持有一份倍率;之后更换operation中的目标,不会修改copied的那份旧状态。若某个目标内部保存共享关系,副本是否仍访问同一个对象要另外沿其捕获方式判断,不能泛化为所有复制都会隔离数据。
#include <functional>
#include <iostream>
int main() {
std::function<int(int)> operation{};
if (!operation) {
std::cout << "empty\n";
}
int factor{4};
operation = [factor](int input) {
return input * factor;
};
auto copied = operation;
factor = 0;
operation = [factor](int input) {
return input * factor;
};
if (operation) {
std::cout << operation(6) << '\n';
}
if (copied) {
std::cout << copied(6) << '\n';
}
return 0;
}
开始operation为空,输出empty。第一份闭包在factor为4时创建并存入operation,copied复制它;factor后来改成0,再创建第二份闭包替换operation。两个条件检查都成功,operation(6)输出0,copied(6)仍输出24。输出顺序是empty、0、24。可调用目标存在与否,仍然不同于其计算结果是否为0。
能调用,不代表能装进这个包装器
C++20的std::function要求其存储目标可复制构造。14.3的移动捕获闭包含unique_ptr,unique_ptr不能复制,因此闭包也不能复制。它仍可通过自己的具体类型调用,但不满足这个包装器的目标要求。
下面反例只编译,预期被拒绝。即使构造包装时写了std::move,也只是让这一次实参参与移动选择;它不会凭空给闭包增加复制能力。错误应发生在构造包装的目标约束上,不能把失败解释成“不认识lambda”或“编译器不支持C++20”。
#include <functional>
#include <memory>
#include <utility>
int main() {
auto owner = std::make_unique<int>(4);
auto callback = [state = std::move(owner)](int input) {
return input * (*state);
};
std::function<int(int)> wrapped{std::move(callback)};
if (wrapped) {
return wrapped(0);
}
return 0;
}
保留独占责任的直接办法,是像完整例二那样返回并保存具体闭包,暂时不使用std::function。不要为了使包装构造通过,改成借用已结束的局部owner。要换拥有模型,必须先有实际的共享责任需求;不该为了消除报错就把所有unique_ptr机械换成shared_ptr。
四步练习阶梯
每一步先留下自己的结果,再展开对照;这些练习属于本章,不另起一套概念讲解。
1. 预测:弱观察和强拥有在哪一行分开?
关闭运行结果,给weak-lock的每条输出标出当时的强拥有者。特别指出held何时建立、何时结束,以及watch为何在Trace销毁后还能报告过期。
对照预测
owners时first和second共两份;remaining时只剩second;locked时增加held。if结束先销毁held,second.reset才结束最后一份并输出destroy 7。watch不保活Trace,但还保存弱观察关系;随后expired 1,新的lock为空,输出empty。
2. 补全:让双向连接可以正常结束
遮住parent-child中Node的两个指针成员声明,按“父负责保活子,子只观察父”补回。预测外部局部名字离开以后两条destroy的次序和两个expired结果,不用手写delete。
对照关键一步
child为std::shared_ptr<Node>,parent为std::weak_ptr<Node>。局部child先结束,但父的成员仍拥有子;随后父析构体输出destroy 1,成员清理释放子,输出destroy 2。外部两个弱观察最终都过期,输出expired 1 1。两个方向都强拥有会形成环,不能靠外部名字消失解除它。
3. 找错:为什么“再加一个move”没有解决?
只编译move-only-function-rejected,记下第一条与目标复制有关的诊断。指出哪个成员导致闭包不可复制,并解释为何构造实参已经std::move仍然被拒绝。无需追踪整段库内部模板展开。
对照原因与修复方向
state是unique_ptr,不能复制,包含它的闭包也不可复制。C++20 std::function对存储目标的复制要求不会因本次实参被移动而取消。保留具体闭包类型即可继续独占资源并安全调用;不要用悬空引用捕获换取编译通过。
4. 独立迁移:返回自己拥有倍率的回调
从空文件写make_multiplier,返回一个具体闭包,移动拥有实际int倍率。分别创建倍率4与0的两个回调,工厂已经返回后各用输入6调用;要求输出24、0,并解释最后谁释放两份倍率。完成后只把创建第一个倍率的实参4改成3,预测新输出。
对照合同
工厂用make_unique建立倍率,返回lambda的初始化捕获state=std::move(owner)接管。main用auto保存具体闭包,不借用工厂局部owner,也不强塞std::function。原输出24、0;唯一实参4改3后输出18、0。各闭包的state在结束时释放自己的倍率;工厂返回不会提前释放已经转交的对象。
运行与检查边界
六份安全源码运行并核对确定输出;第七份move-only-function-rejected只验证预期编译拒绝。两项单条件变体分别是共享值7改0、工厂倍率4改3。强拥有环与逃出的引用回调只检查源码和编译诊断,不运行危险路径,也不声称分析工具证明了任意程序没有泄漏。
继续使用04.1的严格编译方式,替换当前文件名。本章是单线程、普通默认释放方式、C++20标准库的实例;对象寿命与包装器的复制要求以明确合同解释。实际编译输出、自己的预测和独立实现分别保存;通过示例检查不等于通过个人G0。
先留下自己的答案
两道动手练习
先完成正文的练习阶梯,再用下面两题检查自己的解释。先写预测或代码;需要时展开提示,完成后再对照答案。
练习 1
shared-lifetime只把make_shared<Trace>(7)中的7改为0。强拥有者计数和销毁时点是否改变?给出完整四行输出。
查看提示
- 对象保存的数值不参与shared_ptr的强拥有计数。
- first.reset后仍有second,内层结束才销毁Trace。
对照答案与推理
输出owners 2、remaining 0 1、destroy 0、done。计数和作用域顺序不变;值0仍属于一个真实、非空的共享对象。
练习 2
owning-callback只把唯一的make_multiplier(4)改成make_multiplier(3),保留另一倍率0和两个输入6。预测两行输出并解释工厂返回后倍率为什么仍有效。
查看提示
- lambda的state通过移动捕获拥有实际int,未借用工厂局部变量。
- 只改变第一个倍率,不改变第二次调用创建的0。
对照答案与推理
两行变成18、0。第一个闭包现在拥有倍率3,第二个仍拥有倍率0;工厂局部owner结束时已经交出责任,返回闭包的state继续保活各自的int。
关掉参考,再做一次
把理解说出来
先完成正文的独立迁移,再回答下面六题。它们只检验第14.1–14.4节已经讲过的内容。打开答案、编译成功或填写用时,都不会自动通过本章,更不代表通过 G0。
共享关系与最后拥有者
shared-lifetime中两个shared_ptr是否意味着两个Trace?first.reset后为什么没有destroy,remaining后的计数为什么是1?
对照推理与英文回答
两个shared_ptr共享同一个Trace和控制关系,不是复制Trace。first.reset只结束自己的那份强拥有,second仍保活对象,因此可以读7且强拥有计数为1。second离开内层作用域后才销毁这个Trace。这里是固定单线程观察,不据此推断对象数据可并发修改。
The two shared pointers share one Trace and its ownership relationship; they do not create two Trace objects. Resetting first leaves second as the one strong owner. The object is destroyed when second leaves its scope. This is a single-thread observation, not a thread-safety guarantee for the object.弱观察、lock与if作用域
weak-lock里的watch为什么不阻止销毁?if(auto held=watch.lock(); held)中的held又是否保活?expired为false是否等于已拿到可用强拥有者?
对照推理与英文回答
watch是弱观察,不增加强拥有者。成功lock返回的held则是shared_ptr,会在if语句的作用域内保活;该语句结束后held销毁。expired只查询是否过期,不取得强拥有。需要访问时使用lock结果并判断它,不能把一次expired观察当成已经拥有对象。
The weak pointer does not add a strong owner. A successful lock does: held is a shared pointer that keeps the object alive through the if statement’s scope. Expired only observes the relationship; it does not acquire ownership. Access through the checked lock result.父子结构与强拥有环
parent-child为何输出destroy 1再destroy 2,外部两个weak最终都过期?若把子节点的parent成员改成shared_ptr,会怎样?
对照推理与英文回答
原例父对象强持子、子只弱观察父。离开局部块先结束child这个局部强拥有者,子仍由父持有;再结束parent,父析构先输出1,其child成员释放最后一份子强拥有,再输出2。若两个方向都强持,外部拥有者退出后两者仍互相保活,形成未解除的强拥有环;这个错误只分析,不运行。
The parent owns the child, while the child observes the parent weakly. The local child owner ends first; then the parent is destroyed and its child member releases the last child owner, producing one before two. Strong ownership in both directions would keep the cycle alive after external owners leave.初始化捕获与移动捕获
init-capture中factor改为0后为何仍输出24?owning-callback的state和工厂局部owner又是什么关系?
对照推理与英文回答
scale在闭包创建时用factor的4独立初始化,之后改外部factor不会修改scale。state则从std::move(owner)移动初始化unique_ptr,取得释放责任;局部owner变空,state拥有真实倍率对象。两者都不是把外部局部名字借用到将来。
Scale is initialized with the value four when the closure is created, so later changes to factor do not alter it. State is initialized by moving a unique pointer and takes deletion responsibility while the local owner becomes empty. These captures own their stored state rather than borrowing a local name.包装签名、空状态和副本
std::function<int(int)>表达什么调用?callable-wrapper最初为什么先检查空?重新给operation装倍率0的闭包,为何copied仍返回24?
对照推理与英文回答
这个签名要求接收一个int并给出int结果。空包装没有可调用目标,本章先判断、不调用它。copied在operation装倍率4时复制了那个可复制闭包;之后operation被换成另一个倍率0目标,不会把copied中的旧值一起改掉,因此输出0与24。
The signature describes a call taking one int and returning an int. An empty wrapper has no callable target and is not invoked here. Copied stores a copy of the earlier factor-four closure; replacing operation with a factor-zero closure does not change that stored copy, so the results are zero and twenty-four.可调用不等于可复制
move-only-function-rejected中的闭包能算乘法,为何即使用std::move也不能放进C++20 std::function?怎样保留正确的拥有责任?
对照推理与英文回答
闭包含unique_ptr状态,不能复制。C++20 std::function要求存储的目标可复制;把本次构造的实参写成std::move不会让闭包因此获得复制能力。可以像owning-callback一样保留具体闭包类型并返回它,不能改成借用已经结束的工厂局部变量来冒充修复。
The closure is callable but contains a unique pointer and cannot be copied. C++20 std::function requires a copyable stored target; moving the constructor argument does not make that target copyable. Keeping the concrete owning closure preserves the intended lifetime without inventing a dangling reference capture.和正文是同一份源码
示例文件
先自己输入和预测,卡住时再下载对照。文件名相同不代表内容相同;把它们放在单独的练习目录中,避免覆盖自己的作品。
- shared-lifetime.cpp最后一个强拥有者负责销毁
- weak-lock.cpp完整例一:弱观察与暂时取得强拥有
- parent-child.cpp父拥有子,子弱观察父
- init-capture.cpp给捕获状态单独命名和初始化
- owning-callback.cpp完整例二:回调自己拥有倍率
- callable-wrapper.cpp空包装、可复制目标与独立包装副本
- move-only-function-rejected.cpp只编译:不可复制闭包不能装入C++20 std::function · 故意用于编译报错
下载清单来自本章元数据,正文代码与下载同源。六份安全程序用于核对输出;move-only-function-rejected.cpp是预期编译拒绝的反例,只观察诊断,不尝试运行。按各例的输入与验证状态使用;悬空回调等危险讨论只作静态分析,不通过运行未定义行为来猜答案。
可选的学习反馈
记下你真正花的时间
每完成一个学习时段,再填实际分钟。环境准备、阅读推演、独立编码和卡点排查分别记录,避免同一段时间重复计算。离开吃饭或做其他事情的时间不算进去。
记录只保存在你的浏览器,可导出给我复盘。留空表示尚未记录,不等于零耗时;页面停留时间不会自动计为学习。不要把开发者检查时间填进来。
尚无真实试学用时。
换设备:导入记录,或取回损坏的旧记录
导入会合并时段,相同编号不重复累加;发生冲突会保留现有记录。
阅读记录与课程验收分别保存。
本章资料与查证
- WG21 N4861:C++20工作草案 ↗
固定2020-04-01版本。14.1查[util.smartptr.shared.const]、[util.smartptr.shared.dest]、[util.smartptr.shared.obs]与[util.smartptr.shared.create];14.2查[util.smartptr.weak.obs]、[stmt.if]及[class.dtor];14.3查[expr.prim.lambda.capture];14.4查[func.wrap.func.con]、[func.wrap.func.asgn]与[func.wrap.func.inv],以C++20的目标复制要求为准。
- C++ Core Guidelines:R.24 弱关系与共享拥有环 ↗
14.2补充双向强拥有形成环,以及用弱回边表达观察关系的设计动机。本文例子按父强子、子弱父的固定关系展开,不从规则推断所有双向关系都必须共享拥有。
本章独立解释所需读法;资料用于核对与补充。工具版本、操作系统和实际执行状态见自己的运行记录。