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中的每个节点存储一个键的单个字符,可以有多个子节点代表不同的可能的下一个字符。当到达一个键的末尾时,一个标志被设置,以表明其对应的节点是一个结束节点。例子如下:![Trie](https://15445.courses.cs.cmu.edu/fall2022/project0/trie.svg)`<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_ptr
s的所有权,并将自己的 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)