写在前面的:
从这个p开始,我们开始项目实战cmu-15445 p0的编写,需要一些准备工作:
课程官网:https://15445.courses.cs.cmu.edu/fall2023/
github主页:https://github.com/cmu-db/bustub
本人的环境配置使用的是wsl2+clangd(编译)+cmake(构建)+vscode(IDE)+lldb(调试)(wsl1不支持lldb的部分功能,最好升级到2(虽然1的更轻量化),要提供虚拟化,模拟器什么的最好卸了(虽然我没卸,因为mrfz(doge)))还有格式插件clangformat,保持一个良好的码风是很重要的
其他一些无关的插件也最好卸了,以免出bug(环境这部分看个人喜好,喜欢什么用什么,笔者只是提供了个人用的比较舒服的一套)
具体环境搭建可以点这里:https://joytsing.cn/posts/40555/
1. trie.h学习笔记
1.1 MoveBlocked类
class MoveBlocked {
public:
explicit MoveBlocked(std::future<int> wait) : wait_(std::move(wait)) {}
MoveBlocked(const MoveBlocked &) = delete;
MoveBlocked(MoveBlocked &&that) noexcept {
if (!that.waited_) {
that.wait_.get();
}
that.waited_ = waited_ = true;
}
auto operator=(const MoveBlocked &) -> MoveBlocked & = delete;
auto operator=(MoveBlocked &&that) noexcept -> MoveBlocked & {
if (!that.waited_) {
that.wait_.get();
}
that.waited_ = waited_ = true;
return *this;
}
bool waited_{false};
std::future<int> wait_;
};
在 C++ 中,按值传递的过程如下:
- 如果实参是右值,按值传递会调用移动构造函数。
- 如果实参是左值,按值传递会调用拷贝构造函数。
在MoveBlocked类中的应用
- 当对象按值传递时,会触发移动构造函数(如果可用),
std::future
的移动操作会转移资源所有权 - 通过删除拷贝构造函数(MoveBlocked(const MoveBlocked &) = delete),强制使用移动语义
- 在代码中,
std::future
禁用了拷贝构造,只支持移动构造,因此调用时必须传递一个右值(wait_(std::move(wait))
)。
explicit关键字
explicit 关键字用于防止隐式类型转换,在没有 explicit 的情况
class MoveBlocked {
public:
// 没有 explicit
MoveBlocked(std::future<int> wait) : wait_(std::move(wait)) {}
};
// 这时允许以下隐式转换:
std::future<int> fut = std::async([]() { return 42; });
MoveBlocked mb = fut; // 编译通过:fut 被隐式转换为 MoveBlocked
所以我们使用强制显式构造可以让代码更清晰地表达意图,避免在不经意间创建 MoveBlocked 对象
1.2 Trie和TrieNode
// A TrieNode is a node in a Trie.
class TrieNode {
public:
// Create a TrieNode with no children.
TrieNode() = default;
// Create a TrieNode with some children.
explicit TrieNode(std::map<char, std::shared_ptr<const TrieNode>> children) : children_(std::move(children)) {}
virtual ~TrieNode() = default;
// Clone returns a copy of this TrieNode. If the TrieNode has a value, the value is copied. The return
// type of this function is a unique_ptr to a TrieNode.
//
// You cannot use the copy constructor to clone the node because it doesn't know whether a `TrieNode`
// contains a value or not.
//
// Note: if you want to convert `unique_ptr` into `shared_ptr`, you can use `std::shared_ptr<T>(std::move(ptr))`.
virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }
// A map of children, where the key is the next character in the key, and the value is the next TrieNode.
// You MUST store the children information in this structure. You are NOT allowed to remove the `const` from
// the structure.
std::map<char, std::shared_ptr<const TrieNode>> children_;
// Indicates if the node is the terminal node.
bool is_value_node_{false};
// You can add additional fields and methods here except storing children. But in general, you don't need to add
// extra fields to complete this project.
};
// A TrieNodeWithValue is a TrieNode that also has a value of type T associated with it.
template <class T>
class TrieNodeWithValue : public TrieNode {
public:
// Create a trie node with no children and a value.
explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; }
// Create a trie node with children and a value.
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;
}
// Override the Clone method to also clone the value.
//
// Note: if you want to convert `unique_ptr` into `shared_ptr`, you can use `std::shared_ptr<T>(std::move(ptr))`.
auto Clone() const -> std::unique_ptr<TrieNode> override {
return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
}
// The value associated with this trie node.
std::shared_ptr<T> value_;
};
// A Trie is a data structure that maps strings to values of type T. All operations on a Trie should not
// modify the trie itself. It should reuse the existing nodes as much as possible, and create new nodes to
// represent the new trie.
//
// You are NOT allowed to remove any `const` in this project, or use `mutable` to bypass the const checks.
class Trie {
private:
// The root of the trie.
std::shared_ptr<const TrieNode> root_{nullptr};
// Create a new trie with the given root.
explicit Trie(std::shared_ptr<const TrieNode> root) : root_(std::move(root)) {}
public:
// Create an empty trie.
Trie() = default;
// Get the value associated with the given key.
// 1. If the key is not in the trie, return nullptr.
// 2. If the key is in the trie but the type is mismatched, return nullptr.
// 3. Otherwise, return the value.
template <class T>
auto Get(std::string_view key) const -> const T *;
// Put a new key-value pair into the trie. If the key already exists, overwrite the value.
// Returns the new trie.
template <class T>
auto Put(std::string_view key, T value) const -> Trie;
// Remove the key from the trie. If the key does not exist, return the original trie.
// Otherwise, returns the new trie.
auto Remove(std::string_view key) const -> Trie;
// Get the root of the trie, should only be used in test cases.
auto GetRoot() const -> std::shared_ptr<const TrieNode> { return root_; }
};
我们注意到std::shared_ptr<const TrieNode> root_{nullptr};
这句话
它使用了std::shared_ptr
,它的作用类似指针却与引入了reference counting
,让我们来介绍一下它
1. std::shared_ptr
的基本概念
- 自动管理动态分配的对象,避免手动
delete
。 - 引用计数 机制,每个
shared_ptr
维护一个引用计数(use count)。 - 当最后一个
shared_ptr
被销毁时,自动释放资源。 - 支持
std::make_shared<T>()
,可以避免额外的内存分配,提高效率。
2. std::shared_ptr
的基本用法
2.1 创建 shared_ptr
#include <iostream>
#include <memory>
struct Test {
Test() { std::cout << "Test Constructor\n"; }
~Test() { std::cout << "Test Destructor\n"; }
};
int main() {
std::shared_ptr<Test> ptr1 = std::make_shared<Test>(); // 推荐使用 make_shared
{
std::shared_ptr<Test> ptr2 = ptr1; // 引用计数 +1
std::cout << "Use count: " << ptr1.use_count() << std::endl;
} // ptr2 作用域结束,引用计数 -1
std::cout << "Use count: " << ptr1.use_count() << std::endl;
} // ptr1 作用域结束,Test 对象被销毁
输出
Test Constructor
Use count: 2
Use count: 1
Test Destructor
解释:
ptr1
创建了Test
对象,并维护引用计数1
。ptr2
共享ptr1
所管理的对象,引用计数2
。ptr2
离开作用域,引用计数1
。ptr1
离开作用域,引用计数变为0
,对象被释放。
3. 什么时候使用 shared_ptr
?
✅ 适合场景:
- 需要多个对象共享同一个资源,并且需要自动管理生命周期。
- 不能确定何时释放对象,而希望引用计数来自动管理。
❌ 不适合场景:
- 需要唯一所有权时,应该使用
std::unique_ptr
。 - 有循环引用风险的场景,应该使用
std::weak_ptr
。
4. 总结
功能 | shared_ptr |
---|---|
是否自动释放 | ✅ 是 |
是否支持共享 | ✅ 是 |
是否有引用计数 | ✅ 是 |
是否线程安全 | ✅ 线程安全(引用计数是原子操作) |
适合场景 | 资源需要共享,不能确定对象何时销毁 |
💡 最佳实践
- 尽量使用
std::make_shared<T>()
,避免手动new
,减少内存分配开销。 - 函数参数尽量使用
const std::shared_ptr<T>&
,避免不必要的引用计数增加。