换设备了之后 hexo 源文件丢失了,笔者竟然忘记(没意识到)可以在 github 仓库里新开一个 hexo branch 存放源文件。

前言

这篇文章里写好的注册课程提交课程流程笔者就不写了。

CMU 15-445/645 (Spring 2023) Database Systems 通关指北

笔者想学 C++ 但找不到做完会有成就感的项目所以一直耽搁着没学,Modern C++ 的名声在外让笔者同样畏惧。虽然笔者看了许多 C++ 的源码也学会了不少语言,但是 Modern C++ 这座大山一直没敢爬,这是本科的情况。

所以,在研究生的第一年,趁还有三年可供摆烂的时间,开始攀登这座大山。

当头一棒捏

空间截图?.png

Project 0

笔者做的是 2023 年 Fall 的课程,要是笔者能坚持下去的话,大概能和 CMU 的同学们同一时间完成它。

P0 的题目是手写 Trie 树以及利用 Copy-On-Write 让 Trie 树可以同时被多个 Reader 和一个 Writer 访问,这个在原理实在算不上难。让笔者头疼的是 Modern C++ 的语法。现记录如下,可供有一点基础(指除编程外的各科目知识)但也不是完全懂的后来者(大家都是半吊子)参考。

关于 TrieNodeWithValue 的 Clone 函数

按题目要求,在各种赋值的地方应尽量使用 std::move 函数进行所有权变化。首先说明一下 std::move 函数作用:

1
2
3
4
5
6
string s="123";
// 此时 s="123"
string ss=s;
// 此时 s="123",ss="123"
string sss=std::move(s);
// 此时 s="",sss="123"

如上所示,当变量传入 std::move 后,在这之后调用该变量将得到一个空值,换言之,该变量被 move 走了。再说一说有类的成员变量的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
public:
string s;
A(){
s="123";
}
func() -> string{
// 此时 s="123"
return std::move(s);
// 此时 s=""
}
}
auto aa=new A();
// 此时,aa->s="123"
auto ss=std::move(aa->s);
// 此时,aa->s="",ss="123"

可以看到,即使是类的成员变量仍然是可以被 move 走的,无论是外部通过 aa->s 的形式,还是内部调用 s,s 最终都会变为一个空值。这是基础。

现在来看 P0 给出的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TrieNode {
public:
...
explicit TrieNode(std::map<char, std::shared_ptr<const TrieNode>> children) : children_(std::move(children)) {}
...
};

template <class T>
class TrieNodeWithValue : public TrieNode {
public:
...
TrieNodeWithValue(std::map<char, std::shared_ptr<const TrieNode>> children, std::shared_ptr<T> value)
: TrieNode(std::move(children)), value_(std::move(value)) {
this->is_value_node_ = true;
}

auto Clone() const -> std::unique_ptr<TrieNode> override {
return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
}
...
};

TrieNodeWithValue 的 Clone 函数返回了一个 unique_ptr 指向一个新创建的 TrieNodeWithValue 类,我们不妨称原类为类 A,类 A.Clone() 返回的为类 B,类 B 是使用 TrieNodeWithValue(std::map,std::shared_ptr) 这个构造函数进行初始化的,这个构造函数本身又调用了父类 TrieNode(std::map) 进行初始化。

这里的赋值都使用了 std::move 语义,至少笔者之前是这么认为的,所以按照 std::move 的作用,类 A 所有的成员变量都应该已经被 move 走了,换言之,类 A.Clone() 了后,类 A 的所有成员变量都访问不到了。这当然是不行的,毕竟 Clone 函数就不包含这种意思。

笔者冥思苦想(如何询问万能的 ChatGPT),终于找到了问题所在。

事实上,在

1
2
3
auto Clone() const -> std::unique_ptr<TrieNode> override {
return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
}

这句代码中的 std::make_unique(children_,value_);就是通过值复制的方式传递函数参数的,要真的达到笔者说的那种效果,应该将代码改为 std::make_unique(std::move(children_),std::move(value_));,当然,这是错误的,这么做就办不到 COW 了,毕竟内存中就应该存在这个节点的两个备份,一份被读,一份被写。

看上去很麻烦,但也没办法。那么这段代码中的其他 std::move 起到一个什么作用呢?自然就是真正的节省内存了,毕竟无论是 std::map 还是 value 本身,=赋值操作也都是值复制,这里不使用 std::move 应该也没什么问题,就是内存开销大点,过了构造函数那块内存自然就被释放掉了,主要还是课程本身限制必须使用 std::move。