CMU15-445:Project #0 - C++ Primer
Project #0 - C++ Primer
本文是对CMU15-445课程第0个项目文档的一个粗略翻译和总结。仅供个人(M1kanN)复习使用。
1. Overview
本课程的所有编程项目都是在BusTub数据库管理系统上进行的,编程语言采用的是C++。本次项目是C++的一个热身项目。其中,C++的版本是C++17,但是知道C++11的知识点就足够了。
推荐书籍:C++ Primer, Effective Modern C++, A Tour of C++。笔者在做这个项目之前仅仅浏览了C++ Primer,并无C++项目经验。也是希望本次课能提高C++水平。
同时,本项目建议用GDB调试项目。也是对自身能力的一个提高。GDB参考网站:
- Debugging Under Unix: gdb Tutorial
- GDB Tutorial: Advanced Debugging Tips For C/C++ Programmers
- Give me 15 minutes & I'll change your view of GDB [VIDEO]
2. Project Specification
本次项目要求实现一个由并发trie支持的key-value存储。Tries是一种高效的有序树数据结构,用于检索给定键的值。简单起见,我们将假设键是所有非空的可变长度的字符串,但实际上它们可以是任何任意类型。
Trie中的每个节点存储一个键的单个字符,可以有多个子节点代表不同的可能的下一个字符。当到达一个键的末尾时,一个标志被设置,以表明其对应的节点是一个结束节点。例子如下:`<img src="https://15445.courses.cs.cmu.edu/fall2022/project0/graph.png" alt="Trie example" style="zoom: 79%;"暂略 />`
3. Implementation Guide
- InstructionSu我们需要编写的文件名是
p0_trie.h。(位置在src/include/primer/p0_trie.h)
文件指定了函数原型和成员变量,我们只需要编写构造函数,析构函数和成员函数。我们可以添加任何额外的辅助函数和成员变量,但不要修改现有的函数和变量。
Task #1 - Templated Trie暂略
在头文件中,定义了3个我们需要编写的类。建议先编写单线程版本,再编写多线程版本。
TrieNode Class
TrieNode类定义了Trie树的单个结点。TrieNode有3个成员变量:
is_end_:表示是否是字符串的最后一个字符(也就是终结字符,也就是叶子结点!)char:字符键值children_:类型是unordered_map<char, std::unique_ptr<TrieNode>>。注意保存的是unique_ptr,智能指针。用来指向孩子结点。
其中,InsertChildNode与GetChildNode都返回一个指向unique_ptr的指针。这样做的原因是,我们就可以在不复制或者转移所有权的情况下访问unique_ptr的数据。
最后,移动构造函数 TrieNode(TrieNode &&other_trie_node)用来将旧的TrieNode的智能指针转移到新的TrieNode上。请确保你没有在转移数据的时候复制智能指针!
TrieNodeWithValue Class
TrieNodeWithValue继承自 TrieNode,它表示一个路径的终结结点(结点中的 key_char是终结符)。该结点可以存放任意类型(T)的值。并且它的 is_end总Instruction是 true。
当我们用一个给定的键遍历Trie树,并到达结束字符时,我们将根据不同情况来调用 TrieNodeWithValue的不同构造函数(详情见Trie类部分)
目前我们只需要知道 TrieNodeWithValue(char key_c暂略har, T value) 构造器用给定的关键字符和值,从头开始创建一个 TrieNodeWithValue。 (from scratch:从零开始)
TrieNodeWithValue(TrieNode &&trieNode, T value)构造函数从给定的TrieNode中获取 unique_ptrs的所有权,并将自己的 value_设置为给定的值TrieNodeWithValue(TrieNode &&trieNode, T value)。
Trie Class
Trie类定义一个实际的Trie树,支持插入,查找,移除操作。Trie树的根节点是所有键的开始,而且自己不应该存储任何字符值。
-
Insert:
要进行插入操作,我们首先需要用给定键值来遍历Tri暂略e树,如果TrieNode不存在,就插入。注意,插入一个重复的值是不允许的,而且应该返回false。一旦到达了终结结点,一共有3种可能:- 这是一个不存在字符的
TrieNode
这种情况,我们可以调用TrieNodeWithValue(char key_char, T value)构造器来创建给定key_char和value的新节点。请确保一个指向TrieNode的unique_ptr也能够存储一个指向TrieNodeWithValue的unique_ptr。(C++的多态性) - 这是一个有字符的
TrieNode,但是不是一个终结结点。(is_end_ == false)
这意味这个unique_ptr指向一个TrieNode对象,而不是一个TrieNodeWithValued对象。你需要调用TrieNodeWithValue(TrieNode &&trieNode, T value)构造器来转换。 - 这是一个有字符而且也是一个终结结点
这意味着unique_ptr指向TrieNodeWithValue而且我们应该返回错误!因为我们不能插入重复内容。
- 这是一个不存在字符的
-
Remove:
移除一个指定key的步骤:- 基于给定key来遍历Trie树。若key不存在,立刻返回
- 将终结结点的
is_end_设为false。 - 若终结结点无任何孩子,将它从它父结点的
children_map中删除。 - 向上遍历Trie树,递归地删除没有子节点的结点。当遇到一个结点有孩子的时候,停止。
-
GetValue:暂略
给定一个key,返回一个类型T的对应值。若未找到,或者给定的类型不匹配,将success设为false。
注意:为了确认是否两个类型相同,对指向TrieNode的指针,调用强制类型转换dynamic_cast,转换为 指向TrieNodeWithValue<T>的指针。若结果为nullptr,则不匹配。-
原理:
参见另一篇随笔:dynamic_cast 运算符
-
Task #2 - Concurrent Trie
我们需要确保插入、移除、取值操作在多线程环境下工作正常。我们可以使用`RwLatch`(BusTub的读写锁实现),或 `std::暂略shared_mutex`(C++STL库)去实现多线程。
本项目仅需要我们通过获取根节点的读写锁,来实现简单的多线程控制。`GetValue`函数应该获取在根节点上的读锁(通过调用 `RwLatch`的 `RLock方法`),`Insert`和 `Remove`操作需要获取在根节点的写锁。
如果我们用了`RWLatch`,请确保解锁了所有的锁,以避免死锁。
4. Instruction
注意!
本篇是在笔者完成实验后才写的一个记录性随笔。不负责步骤顺序的正确性!(可能有遗漏)
请不要完全参考这篇文章!!!!请去官网的Instruction一步一步跟着做!!!!
Creating Your Own Project Repository
笔者用的是Ubuntu22.4系统
-
首先需要具备一定的git知识,并安装git。linux系统一般都有git。
-
注册github账号,然后创建新仓库。并从官方的github上fork代码。注意一定要设为private仓库。
$ git remote add \ public https://github.com/cmu-db/bustub.git用下列代码来跟踪最新版代码:
$ git fetch public $ git merge public/master
Setting Up Your Development Environment
-
Linux下:
sudo build_support/packages.sh -
接着创建build文件夹。进行make操作
$ mkdir build $ cd build $ cmake -DCMAKE_BUILD_TYPE=Debug .. $ make -
这里建议开启CMake的Debug模式,这样就既可以test也可以检查内存泄露
$ cmake -DCMAKE_BUILD_TYPE=DEBUG .. -
要加速make可以用这个指令,加上多线程
$ make -j$(nproc)
Testing
-
写完代码就可以开始测试了:
用来测试的文件在test/primer/starter_trie_test.cpp里面。把想测试的类,函数参数名字的前缀DISABLED_去掉,就可以指定测试了!$ cd build $ make starter_trie_test $ ./test/starter_trie_test -
我们 可以运行
make check-tests命令来运行全部测试例子。由于我们才implement一个project,所以肯定会有很多fails。
Formatting
-
本课程用的代码规范是:
Google C++ Style Guide -
用的是 Clang 来自动化检查代码规范性。
命令行输入:$ make format $ make check-lint $ make check-clang-tidy-p0来测试代码。其中
make check-lint是用来检测细节的(也就是一些排版的错误)。make check-clang-tidy-p0可以检测用的语法有没有问题,有没有warning之类的。
Memory Leaks
-
本课程用的是:LLVM Address Sanitizer (ASAN) and Leak Sanitizer (LSAN)
开启Debug模式会自动检测。 -
也可以用 Valgrind 来检测。
运行:valgrind \ --error-exitcode=1 \ --leak-check=full \ ./test/starter_trie_test -
这里笔者没有多此一举,直接用Debug模式就行了。后面遇到严重的问题再来试试看valgrind。
Development Hints
-
建议不要用
printf来debugging,用LOG_ *宏来调试更好!例子:LOG_INFO("# Pages: %d", num_pages); LOG_DEBUG("Fetching page %d", page_id); -
为了启用logging,我们应该加上
-DCMAKE_BUILD_TYPE=Debug。也就是启用debug模式,才能用LOG。 -
使用LOG的一些注意事项:
The different logging levels are defined in
src/include/common/logger.h. After enabling logging, the logging level defaults toLOG_LEVEL_INFO. Any logging method with a level that is equal to or higher thanLOG_LEVEL_INFO(e.g.,LOG_INFO,LOG_WARN,LOG_ERROR) will emit logging information. Note that you will need to add#include "common/logger.h"to any file in which you want to make use of the logging infrastructure. -
建议用gdb来调试。
笔者后面会写一篇gdb的来学习。挖个坑。
Submission
- 在AutoGrade上提交代码:
https://www.gradescope.com/courses/424375/
打包提交就行。 - FAQ中有注册码。
5. Implementation Note
遇到的问题
move和forward的应用
注意左值右值。以及是传递左值还是传递右值。noexcept的运用
一般只用在移动构造函数中。声明不产生except。unique_ptr的使用get()成员函数
获得原来的指针make_unique()
多使用make_unique! 养成习惯reset(q)
将指针设为q。如果q 为空则是置为空。- 注意:
由于unique_ptr的特性,我们经常使用一个指针来指向智能指针!!!这样就可以防止对unique_ptr的复制了
unordered_map的应用erase()删除某个结点
- 读写锁
这里返回的时候要解锁就ok了
测试和提交

- 语法测试的时候:
注意不要改变代码的逻辑。不然就会前功尽弃了。
6. Summary
本次项目只是实现了一个Trie树。算是对课程项目的一个热身。不熟悉Cpp的同学也可以借此学习一些Cpp的知识。
7. Reference
[1] 字典树(Trie)

浙公网安备 33010602011771号