project0一共要用到这几个文件,再加个tire_store.h
路径:src/include/primer
src/primer
task1 要用到tire.h,tire.cpp.tire_test.cpp
tire.h:头文件,需要差不多读懂,需要用到一些变量名
tire.cpp:在这个里面写代码
tire_test.cpp:在tire.cpp完成后,运行这个,可以看到自己的代码有没有问题。
1、tire.h头文件解读
1.1整体
bustub空间内,一共四个类(class)
MoveBlocked 类:一个特殊类型,用于阻止移动构造和移动赋值操作。在Trie的测试中使用,以确保对象不会被意外移动(不用细看)
TireNode类:
—提供构造函数来创建Trie节点(没有子节点->TrieNode() = default;
或者有子节点 ->explicit TrieNode(std::map<char, std::shared_ptr> children) : children_(std::move(children)) {} )。
—一个虚拟的Clone方法,用于复制Trie节点。
–一个表示子节点的映射
—一个标志结点内是否有值的bool变量(is_value_node_),默认没有值(false)。
TrieNodeWithValue类:继承TrieNode类,创建了能储存值的结点;重写了Clone方法,以确保值也被复制。
Tire类:一个指向根节点的智能指针;提供操作方法,如获取、插入、移除。
1.2 细节
1
explicit TrieNode(std::map<char, std::shared_ptr<const TrieNode>> children) : children_(std::move(children)) {}
explicit关键字用于阻止C++进行隐式类型转换,强制要求传入参数的类型必须符合声明
std::map<int,string> personnel; 一个用int作为索引,并拥有相关联的指向string的指针,叫personnel
const:阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化,以后就没有机会改变他了;
shared_ptr是一种智能指针(smart pointer),作用有如同指针,但会记录有多少个shared_ptr共同指向一个对象。这便是所谓的引用计数(reference counting)。
一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。
std::move()将对象的状态或所有权转换给另一对象,这里是调用构造函数时传入的对象children到children_,之后传入的对象就为空了,这样做只是转移,没有内存的搬迁或者内存拷贝,如果不加则需要先将传入对象拷贝一份为临时对象,然后给属性成员赋值,之后销毁临时对象。因此,通过std::move(),可以避免不必要的拷贝操作。这样的构造函数称为移动构造函数
函数(类型 参数名):属性成员名(参数名){}等价于函数(类型 参数名){this.属性成员名=参数名;}
函数:TireNode
类型:std::map<char, std::shared_ptr>
参数名:children
把函数传过来的参数(children),赋值给该属性成员(children_): : children_(std::move(children))
2
virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }
auto 函数() -> 类型{…} 作用是推导出函数的返回类型
unique_ptr:一种独占式智能指针,它确保了只有一个指针可以指向资源,可以通过std::move()函数将资源的所有权转移给其他unique_ptr对象。它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
std::make_unique:是C++14中引入的一个函数,用于创建一个唯一指针(unique_ptr),这是一种更安全、更简洁的方式来创建动态分配的对象。
ps:
std::make_shared:共享用智能指针
2、写代码时要用到的知识
2.1 智能指针
前面有过涉及,但是这里做一个总结
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。
例如,使用普通指针来动态分配一个整型数组:
int* arr = new int[10];
// 使用 arr 操作数组
delete[] arr; // 手动释放内存
使用智能指针 std::unique_ptr 来管理同样的数组:
std::unique_ptr<int[]> arr(new int[10]);
// 使用 arr 操作数组
// 不需要手动释放内存,智能指针会自动释放
2.2 类型转换 dynamic_cast与dynamic_pointer_cast(task 1)
语法:std::dynamic_cast<目标类型*>(被转换对象)
std::dynamic_pointer_cast<目标类型>(被转换智能指针)
dynamic_pointer_cast与dynamic_cast的区别
适用对象类型:dynamic_pointer_cast适用于智能指针类型,如std::shared_ptr和std::weak_ptr,用于进行智能指针的动态类型转换。而dynamic_cast适用于指针和引用类型,用于进行指针或引用类型的动态类型转换。
用法:dynamic_pointer_cast是一个函数模板,接受两个参数,分别是要转换的指针和目标类型的指针类型。它返回一个智能指针类型的结果。而dynamic_cast是一个运算符,在使用时需要使用dynamic_cast<目标类型>(被转换对象)的语法。它返回一个指针或引用类型的结果。
转换失败处理:dynamic_pointer_cast在转换失败时,会返回一个空智能指针(nullptr)。而dynamic_cast在转换失败时,如果转换是指针类型,会返回一个空指针(nullptr);如果转换是引用类型,会抛出std::bad_cast异常。
适用范围:dynamic_pointer_cast只适用于智能指针类型,因此只能用于具有动态多态性的对象。而dynamic_cast可以用于具有动态多态性的对象,也可以用于普通的指针和引用类型。
2.3 加锁解锁lock_guard和unique_lock(task 2)
语法:std::lock_guard guard(mt);
unique_lock unique(mt); unique.unlock(); 解锁
区别:unique_lock unique(mt);会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候会自动解锁,也没有中途解锁的功能。
3、代码
非递归https://blog.csdn.net/qq_58887972/article/details/135708757?spm=1001.2014.3001.5502
递归 https://blog.csdn.net/Aft3rGl0w/article/details/135704274
主要参考的这两个代码,自己写的内容很少,主要集中在注释
3.1 task1
实现树节点的查找(Get 方法)、添加修改(Put方法),删除(Remove 方法)
Get方法
template <class T>
auto Trie::Get(std::string_view key) const -> const T * {
// 从根节点开始
auto node = root_;
// 按输入的key值遍历Trie树
for (char ch : key) {
// 如果当前节点为nullptr,或者没有对应字符的子节点,返回nullptr,程序结束
if (node == nullptr || node->children_.find(ch) == node->children_.end()) {
return nullptr;
}
// 当前字符存在,接着往后面找
node = node->children_.at(ch);
}
// 路径存在,检查类型是否匹配
// 将节点转换为TrieNodeWithValue<T>类型
const auto *value_node = dynamic_cast<const TrieNodeWithValue<T> *>(node.get());
// 如果转换成功并且节点包含正确类型的值,则返回该值的指针
if (value_node != nullptr) {
return value_node->value_.get();
}
// 若类型不匹配,返回nullptr
return nullptr;
// You should walk through the trie to find the node corresponding to the key. If the node doesn't exist, return
// nullptr. After you find the node, you should use `dynamic_cast` to cast it to `const TrieNodeWithValue<T> *`. If
// dynamic_cast returns `nullptr`, it means the type of the value is mismatched, and you should return nullptr.
// Otherwise, return the value.
}
Put方法
//put的递归方法
// Put:递归方法具体实现
template <class T>
void PutCycle(const std::shared_ptr<bustub::TrieNode> &new_root, std::string_view key, T value) {
// 判断元素是否为空的标志位
bool flag = false;
// 在new_root的children找key的第一个元素
// 利用for(auto &a:b)循环体中修改a,b中对应内容也会修改及pair的特性
for (auto &pair : new_root->children_) {
// 如果找到了
if (key.at(0) == pair.first) {
flag = true;
// 剩余键长度大于1
if (key.size() > 1) {
// 复制一份找到的子节点,然后递归对其写入
std::shared_ptr<TrieNode> ptr = pair.second->Clone();
// 递归写入 .substr(1,key.size()-1)也可以
// 主要功能是复制子字符串,要求从指定位置开始,并具有指定的长度。
PutCycle<T>(ptr, key.substr(1), std::move(value));
// 覆盖原本的子节点
pair.second = std::shared_ptr<const TrieNode>(ptr);
} else {
// 剩余键长度小于等于1,则直接插入
// 创建新的带value的子节点
std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
TrieNodeWithValue node_with_val(pair.second->children_, val_p);
// 覆盖原本的子节点
pair.second = std::make_shared<const TrieNodeWithValue<T>>(node_with_val);
}
return;
}
}
if (!flag) {
// 没找到,则新建一个子节点
char c = key.at(0);
// 如果为键的最后一个元素
if (key.size() == 1) {
// 直接插入children
std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
new_root->children_.insert({c, std::make_shared<const TrieNodeWithValue<T>>(val_p)});
} else {
// 创建一个空的children节点
auto ptr = std::make_shared<TrieNode>();
// 递归
PutCycle<T>(ptr, key.substr(1), std::move(value));
// 插入
new_root->children_.insert({c, std::move(ptr)});
}
}
}
template <class T>
auto Trie::Put(std::string_view key, T value) const -> Trie {
// 第一种情况:值要放在根节点中,无根造根,有根放值
// 更改根节点的值,即测试中的 trie = trie.Put<uint32_t>("", 123);key为空
if (key.empty()) {
std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
// 建立新根
std::unique_ptr<TrieNodeWithValue<T>> new_root = nullptr;
// 如果原根节点无子节点
if (root_->children_.empty()) {
// 直接修改根节点
new_root = std::make_unique<TrieNodeWithValue<T>>(std::move(val_p));
} else {
// 如果有,把原根的关系转移给新根:root_的children改为newRoot的children
// 这里看tire.h里TireNodeWithValue方法,可以看到,传入不同数量的参数,对应实现不同的方法。
new_root = std::make_unique<TrieNodeWithValue<T>>(root_->children_, std::move(val_p));
}
// 返回新的Trie
return Trie(std::move(new_root));
}
// 第二种情况:值不放在根节点中
// 2.1 根节点如果为空,新建一个空的TrieNode;
// 2.2 如果不为空,调用clone方法复制根节点
std::shared_ptr<TrieNode> new_root = nullptr;
if (root_ == nullptr) {
new_root = std::make_unique<TrieNode>();
} else {
new_root = root_->Clone();
}
// 递归插入,传递根节点,要放的路径:key, 要放入的值:value
PutCycle<T>(new_root, key, std::move(value));
// 返回新的Trie
return Trie(std::move(new_root));
}
Remove方法
不仅要删除键值,还需要在删除后清除所有不必要的节点。递归遍历key,如果发现当前的key的元素不在当前递归的trie节点的子节点映射中,则说明trie没有这个键,直接返回false表示没有移除任何键值。
如果当前的key的元素在当前递归的trie节点的子节点映射中,继续判断这是不是key的最后一个元素(遍历到终点),如果遍历到终点,则判断key节点是否有子节点,如果没有子节点则说明可以直接删除key节点,如果有子节点则不能删除,只能将key节点转为无value的普通节点,然后返回true表示删除了一个键值。
如果没有遍历到终点,和Put逻辑一样,拷贝当前的key[i]在当前递归的trie节点的子节点映射的节点,然后以拷贝得到的节点继续递归。获取递归的结果,如果为false,则说明没有删除任何节点,直接返回false,否则判断当前节点是否可删除(是否为value节点 or 是否有子节点),如果可删除则删除当前节点并返回true。 (原文链接:https://blog.csdn.net/Aft3rGl0w/article/details/135704274)
//remove 递归移除方法
// remove 递归移除方法
bool RemoveCycle(const std::shared_ptr<TrieNode> &new_roottry, std::string_view key) {
// 在new_root的children找key的第一个元素
for (auto &pair : new_roottry->children_) {
// 继续找
if (key.at(0) != pair.first) {
continue;
}
if (key.size() == 1) {
// 是键结尾
if (!pair.second->is_value_node_) {
return false;
}
// 如果子节点为空,直接删除
if (pair.second->children_.empty()) {
new_roottry->children_.erase(pair.first);
} else {
// 否则转为TireNode
pair.second = std::make_shared<const TrieNode>(pair.second->children_);
}
return true;
}
// 拷贝一份当前节点
std::shared_ptr<TrieNode> ptr = pair.second->Clone();
// 递归删除
bool flag = RemoveCycle(ptr, key.substr(1, key.size() - 1));
// 如果没有可删除的键
if (!flag) {
return false;
}
// 如果删除后当前节点无value且子节点为空,则删除
if (ptr->children_.empty() && !ptr->is_value_node_) {
new_roottry->children_.erase(pair.first);
} else {
// 否则将删除的子树覆盖原来的子树
pair.second = std::shared_ptr<const TrieNode>(ptr);
}
return true;
}
return false;
}
auto Trie::Remove(std::string_view key) const -> Trie {
if (this->root_ == nullptr) {
return *this;
}
// 键为空
if (key.empty()) {
// 根节点有value
if (root_->is_value_node_) {
// 根节点无子节点
if (root_->children_.empty()) {
// 直接返回一个空的trie
return Trie(nullptr);
}
// 根节点有子节点,把子结点转给新根
std::shared_ptr<TrieNode> new_root = std::make_shared<TrieNode>(root_->children_);
return Trie(new_root);
}
// 根节点无value,直接返回
return *this;
}
// 创建一个当前根节点的副本作为新的根节点
std::shared_ptr<TrieNode> newroot = root_->Clone();
// 递归删除
bool flag = RemoveCycle(newroot, key);
if (!flag) {
return *this;
}
if (newroot->children_.empty() && !newroot->is_value_node_) {
newroot = nullptr;
}
return Trie(std::move(newroot));
// You should walk through the trie and remove nodes if necessary. If the node doesn't contain a value any more,
// you should convert it to `TrieNode`. If a node doesn't have children any more, you should remove it.
}
3.2 task2
多线程环境实现一个并发控制的键值存储,task1是实现单线程的,task2会用到task1写的方法
tire_store.h中一共两把锁root_lock_ 用于 访问 ,write_lock_ 用于读写
Get方法
get方法按照作业自带的要求写就好了(代码中的英文部分)
template <class T>
auto TrieStore::Get(std::string_view key) -> std::optional<ValueGuard<T>> {
// Pseudo-code:
// (1) Take the root lock, get the root, and release the root lock. Don't lookup the value in the
// trie while holding the root lock.
// (2) Lookup the value in the trie.
// (3) If the value is found, return a ValueGuard object that holds a reference to the value and the
// root. Otherwise, return std::nullopt.
std::shared_ptr<const TrieNode> root_copy;
auto trie = Trie();
// 使用互斥锁来保护根节点的读取
std::lock_guard<std::mutex> lock(root_lock_);
// 获取Trie的根节点
trie = root_;
// 在Trie中查找键
const T *value = trie.Get<T>(key); // 假设TrieNode有Get方法
// 根据查找结果返回
if (value) {
return ValueGuard<T>(trie, *value);
}
return std::nullopt;
}
Put方法
put和remove方法需要上两把锁
write_lock_(读写锁)防止其他写进程写入trie,然后向root中放入key-value
root_lock_ (访问锁)放入或移除完成后需要更新trie,所以需要避免其他进程读。更新原始trie后释放锁
template <class T>
void TrieStore::Put(std::string_view key, T value) {
// You will need to ensure there is only one writer at a time. Think of how you can achieve this.
// The logic should be somehow similar to `TrieStore::Get`.
std::shared_ptr<const TrieNode> root_copy;
// lock_guard 加锁,当离开局部作用域,析构函数自动完成解锁功能
// 读写加锁
std::lock_guard<std::mutex> lock(write_lock_);
// 复制当前根节点,对其进行修改
Trie new_trie = root_.Put<T>(key, std::move(value));
std::lock_guard<std::mutex> root_lock(root_lock_);
root_ = new_trie;
}
Remove方法
void TrieStore::Remove(std::string_view key) {
// You will need to ensure there is only one writer at a time. Think of how you can achieve this.
// The logic should be somehow similar to `TrieStore::Get`.
// 访问和读写一起加锁
std::lock_guard<std::mutex> lock(write_lock_);
// 执行删除操作并获取新的 Trie 实例
Trie new_trie_version = root_.Remove(key);
std::lock_guard<std::mutex> root_lock(root_lock_);
// 更新 Trie 的根节点
root_ = new_trie_version;
}
3.3 task3
task3 在注释要求的地方添加断点,根据要求出三个数,放在tire_answer.h中
(1)有几个孩子节点 :children下[0]-[9]一共10个
(2)9号节点有几个孩子节点:1个
(3)“969”路径对应的值是什么:(char)57=9,可以看到值是25
3.4 task4
在string_expression.h和plan_func_call.cpp中实现大小写函数。没有c++基础写之前我还疑惑为什么大家都不写,实现了以后确实不难
string_expression.h
auto Compute(const std::string &val) const -> std::string {
// TODO(qijiarui): implement upper / lower.
std::string ret;
switch (expr_type_) {
case StringExpressionType::Lower:
for (auto c : val) {
ret.push_back(std::tolower(c));
}
break;
case StringExpressionType::Upper:
for (auto c : val) {
ret.push_back(std::toupper(c));
}
break;
default:
break;
}
return ret;
}
plan_func_call.cpp
auto Planner::GetFuncCallFromFactory(const std::string &func_name, std::vector<AbstractExpressionRef> args)
-> AbstractExpressionRef {
// 1. check if the parsed function name is "lower" or "upper".
// 2. verify the number of args (should be 1), refer to the test cases for when you should throw an `Exception`.
// 3. return a `StringExpression` std::shared_ptr.
if ((func_name == "lower" || func_name == "upper") && args.size() == 1) {
return static_cast<std::shared_ptr<StringExpression>>(std::make_shared<StringExpression>(
args[0], func_name == "lower" ? StringExpressionType::Lower : StringExpressionType::Upper));
}
throw Exception(fmt::format("func call {} not supported in planner yet", func_name));
}
4 提交
本地运行通过了我以为打包好提交就结束了,结果这个地方卡了两天。把可能出问题的地方说一下
4.1流程
注册
进入这个网址
https://www.gradescope.com/courses/579715
Course Entry Code输入:KK5DVJ(2023fall) 其余年份可以去这里面找,,网址改为对应年份的就行: https://15445.courses.cs.cmu.edu/fall2023/faq.html
学校输入:Carnegie Mellon University
最后界面如下图:
执行格式检查
安装clang-fromat,clang-tidy
sudo apt-get install clang-format
下方语句需管理员权限(输入su和密码)
apt-get install clang-tidy
然后在build文件夹下依次执行以下命令
make format
make check-lint
make check-clang-tidy-p0
可能会报错,按照提示更改自己的文件就好
打包
(官网教程内容)在build文件夹下调出终端输入
make submit-p0
调到上层目录 cd …
输入python3 gradescope_sign.py
这里会让你输入很多东西,邮箱,姓名,githubID等等,按提示输入就好
完成后提交到网站
4.2遇见问题
问题1:
压缩包里的内容正常应该是trie_store.cpp等等一系列cpp和.h文件,但我的压缩包里只有orset.h和orset.cpp文件
解决方法1
打开build文件夹内的makefile文件,找到submit-p0对应的文件路径(308行)
看第69、70行,我之前的只有orset路径,没有tire_store那些,更改后就可以了
cd /home/qijiarui/bustub-gun && zip project0-submission.zip src/planner/plan_func_call.cpp src/include/execution/expressions/string_expression.h src/include/primer/trie.h src/include/primer/trie_store.h src/include/primer/trie_answer.h src/primer/trie.cpp src/primer/trie_store.cpp
问题2:
即使之前进行了格式检查,实际放到线上时,格式检查也会不通过。(分数是0)
解决办法2:
按照线上提示的内容进行修改,他会上一行给出问题语句,下一行给出怎么改的建议,按照建议来就可以。莫慌
通过截图
放在结尾的一些碎碎念
这个项目网上找了好多代码,自己写的很少,之后还要再看两天代码,把不了解的写法总结一下,但是总算在腊月二十九的晚上写完了,不然大年三十调代码也太绝望了。这一个项目算上之后的两天做了整整半个月,c++一无所知的我觉得好难啊,那些半个月做5个项目的是什么神仙,只能用好歹也一直在进步安慰自己了。就祝自己新年快乐,新的一年事事顺心吧!
参考文章
[1]https://blog.csdn.net/baidu_31541363/article/details/95802210(std::shared_ptr 详解)
[2]https://blog.csdn.net/Falling_Asteroid/article/details/1339636549(P0 C++Primer)
[3]https://www.cnblogs.com/spruce/p/13095720.html(std::map(转))
[4]https://blog.csdn.net/qq_58887972/article/details/135708757?spm=1001.2014.3001.5502(CMU-15445project0(仅自己记录留念))
[5]https://blog.csdn.net/yi_chengyu/article/details/121921622(【C/C++】C++11 智能指针与普通指针重要区别)
[6]https://blog.csdn.net/Aft3rGl0w/article/details/135704274(CMU 15445 2023fall #Project0 实现一个简单的k-v存储引擎)
[7]https://zhuanlan.zhihu.com/p/340348726(C++11多线程编程(三)——lock_guard和unique_lock)
[8]https://blog.csdn.net/weixin_43721070/article/details/122638851(C++代码自动检测工具clang-format和clang-tidy)
[9]https://zhuanlan.zhihu.com/p/622663820(【CMU15-445 FALL 2022】Project #0 - C++ Primer)