CMU 15445 Projec0 2022

前言

CMU 15445是CMU针对数据库内核的一门硬核课程,区别于传统国内教学数据库教授SQL 增删查改。对于进一步理解并能开发数据库有极强的帮助作用。
所需知识

  • C++ 11
  • 算法与数据结构
  • your patience and enthusiasm

实验简介

官方指导网页

https://15445.courses.cs.cmu.edu/fall2022/project0/

实验中采用的数据结构是字典树,一种经典的KV存储的数据结构
在这里插入图片描述
在这里插入图片描述
实验中用到了三个核心class

  • TrieNode a single node in a Trie. A TrieNode holds one character of a key and the is_end_ flag indicates whether it marks the end of a key string.
    在这里插入图片描述
  • TrieNodeWithValue TrieNodeWithValue class inherits from TrieNode and represents a terminal node (node whose key_char is the ending character of a key) that can hold values of arbitrary type T. Its is_end_ flag is always true.
  • Trie Class
    整个存储字典树 包含插入删除等功能

TrieNode

填补相对应的函数block

构造函数

在这里插入图片描述
什么是C++中的显式转换和隐式转换呢?以及为什么要在代码中指定只能显式转换
在这里插入图片描述
常见的在C++中还有reinterpret_cast这一显式转换,用于执行底层二进制的重新解释转换。它可以将一个指针或引用转换为不同类型的指针或引用,或者将整数类型转换为指针类型,反之亦然。
在这里插入图片描述

移动构造函数1

在这里插入图片描述
实现转换功能有两种方式分别是 swap和move函数,二者之间的区别?

1:在这种情况下,this->children_ = std::move(other_trie_node.children_)this->children_.swap(other_trie_node.children_) 在运行效果上是相同的,因为它们都将other_trie_node的children_移动到了当前对象的children_中。

std::moveother_trie_node.children_ 转换为右值引用,然后通过赋值运算符将其移动到this->children_。这将使this->children_拥有other_trie_node之前的数据,并且other_trie_node的children_会被置为空状态。

2:另一方面,this->children_.swap(other_trie_node.children_)通过调用std::unordered_map的swap函数交换了this->children_other_trie_node的children_。这将导致this->children_指向other_trie_node之前的数据,而other_trie_node的children_指向this->children_之前的数据。

总结:在这个特定的情况下,两种方式的运行效果是一样的,它们都将other_trie_node的children_移动到了当前对象的children_中。然而,值得注意的是,两种方式的实现方式略有不同,可能会涉及不同的底层操作,因此在具体使用时需要根据具体情况选择适合的方式。

更好理解move函数,区别于Rust所有权转换

std::move 并不直接进行数据的移动操作,它只是将对象标记为可移动的右值引用。这样做的目的是告诉编译器,我们希望使用移动语义来处理该对象,而不是复制语义。
当使用std::move标记对象时,它仅仅是改变了对象的类型,将左值引用转换为右值引用。这样做的结果是,编译器可以选择使用移动语义而不是复制语义来处理对象。
在上述代码中,this->children_ = std::move(other_trie_node.children_)将other_trie_node.children_转换为右值引用,并将其赋值给this->children_。这样做的好处是,编译器可以选择移动语义来处理对象的数据,从而提高效率。
请注意,std::move本身并不执行实际的数据移动,它只是提供了一种机制,告诉编译器我们希望使用移动语义。实际的数据移动操作发生在赋值运算符的实现中,具体如何处理数据移动取决于对象的类型和实现。

为什么出现了&& 是右值了,但是不能直接等于还需要进行std::move函数呢?
在这里插入图片描述
看到了&& (右值引用的标志是&&),我们会第一时间认为是右值,但是实际上并不是 ,一般如果作为函数返回值的 && 是右值,直接声明出来的 && 是左值。所以这里的other_trie_node在使用的时候仍然是左值

简单函数(无强调之处)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

InsertChildNode 函数

要求
在这里插入图片描述
代码如下
在这里插入图片描述
我们返回的是一个指针指向unique_ptr,这样我们就可以在不转换所有权的情况下,通过访问指针来进行数据的访问
move函数和forward函数
在这里插入图片描述

TrieNodeWithValue

在这里插入图片描述
继承from TrieNode ,采用泛型的原因是,当进入结尾是,key指向的数据可以是任意类型,如图所示
在这里插入图片描述

TrieNodeWithValue(TrieNode &&trieNode, T value)

在这里插入图片描述
在C++11及以后的版本中,当对象以右值引用(&&)的形式传递给构造函数时,编译器会自动调用移动构造函数(如果存在),而不是拷贝构造函数
在这里插入图片描述
还有一种写法采用完美转发函数

如何理解移动构造函数和拷贝构造函数
在C++11及以后的版本中,当对象以右值引用(&&)的形式传递给构造函数时,编译器会自动调用移动构造函数(如果存在),而不是拷贝构造函数。

  1. 拷贝构造函数
    拷贝构造函数用于创建一个新对象,该对象与给定对象具有相同的属性和值。拷贝构造函数的形式通常为类名(const 类名& other),其中other是同类的另一个对象。它会深度复制other对象的内容,并创建一个新的对象。
  2. 移动构造函数
    移动构造函数用于创建一个新对象,并从给定对象“移动”(而不是复制)资源,例如动态分配的内存或资源所有权。移动构造函数的形式通常为类名(类名&& other),其中other是同类的右值引用。移动构造函数的目的是通过转移资源的所有权来提高性能和效率。
    在这里插入图片描述

Tire

Trie is a concurrent key-value store. Each key is a string and its corresponding value can be any type.
构造函数
在这里插入图片描述
reset设置指针的重定向

Insert函数

要求:
1:如果字符串是空,立即返回false
2:如果该string已经存在 ,说明不能插入,不可以修改已经存储的value
3:如果该字符串不存在,插入,并调用构造函数,将终端节点修改成TrieNodeWithValue类型
4:如果存在,并终端节点TrieNode,转换成TrieNodeWithValue
5:如果存在,并终端节点TrieNodeWithValue,返回错误,即第二点
在这里插入图片描述

        template <typename T>
        bool Insert(const std::string &key, T value) {
            if (key.empty()) return false;
            latch_.WLock(); //插入时,写锁lock
            auto node = &root_;
            if(key.size()>1) {
                for (auto ch : key.substr(0, key.size() - 1)) {
                    auto child = node->get()->GetChildNode(ch);
                    if (child == nullptr)
                        child = node->get()->InsertChildNode(ch, std::make_unique<TrieNode>(ch));
                    //std::unique_ptr<TrieNode> *InsertChildNode(char key_char, std::unique_ptr<TrieNode> &&child)
                    node = child;
                }
            }
            //到倒数第二个字符
            bool ret=false;
            char ch = key[key.size() - 1];
            auto terminal_node = node->get()->GetChildNode(ch); //判断最后一个是不是倒数第二个的子节点
            if (terminal_node == nullptr)  //
            {
                node->get()->InsertChildNode(ch, std::make_unique<TrieNodeWithValue<T>>(ch, value));
                ret=true;
            } else if (!terminal_node->get()->IsEndNode())  // 不是叶子节点
            {
                auto new_node_terminal = new TrieNodeWithValue<T>(std::move(*(terminal_node->get())), value);
                // 创建一个新的TrieNodeWithValue<T>类型的对象,
                // 并将terminal_node指向的对象的值转移到该对象的构造函数中,同时传递value作为另一个参数。
                // 如果相应节点存在但不是终结节点(通过 is_end_ 判断)
                // 将其转化为 TrieNodeWithValue 并把值赋给该节点
                // 该操作不破坏以该键为前缀的后续其它节点(children_ 不变),插入成功
                terminal_node->reset(new_node_terminal);
                ret=true;
            } else {  // 如果相应节点存在且是终结节点,说明该键在 Trie 树存在,规定不能覆盖已存在的值,返回插入失败
                ret= false;
            }
            latch_.WUnlock();
            return ret;
        }

remove()

// 如果其 parent 节点在移除该子节点后没有其它子节点,也删除
        bool Remove(const std::string &key) {
            if (key.empty()) return false;
            latch_.WLock();
            std::unique_ptr<TrieNode> *node = &root_;
            std::vector<std::unique_ptr<TrieNode> *> traversal_path;
            for (auto ch : key) {
                traversal_path.emplace_back(node);
                auto child = node->get()->GetChildNode(ch);
                if (child == nullptr) {  // 没有该映射
                    latch_.WUnlock();
                    return false;
                }
                node = child;
            }
            if (node->get()->HasChildren())  // 没有到达叶子节点位置 说明她是某个字符串的前缀
                node->get()->SetEndNode(false);
            else {  // 回溯删除相应的映射
                for (int i = traversal_path.size() - 1; i >= 0; --i) {
                    auto pre = traversal_path[i];
                    if (node->get()->IsEndNode() || node->get()->HasChildren())
                        // 如果是,则表示当前 Trie 树节点不应该被删除,因为它可能是其他单词的前缀节点,
                        //  或者它的子节点可能是其他单词的结束节点。
                        //  所以在回溯删除相应的映射时,如果当前 Trie 树节点满足这两个条件之一,会跳出循环,
                        //  不执行删除操作,以保留这个节点。
                        break;  // if the key exists and is removed
                    pre->get()->RemoveChildNode(key[i]);
                    node = pre;
                }
            }
            latch_.WUnlock();
            return true;
        }

getvalue()

在这里插入图片描述

/*沿 Trie 树查找,如果键不存在,或者节点中存储的值类型与函数调用的类型 T 不一致,
         * 将 *success 标识设为 false。类型判断的方式是使用 dynamic_cast。
         */
        template <typename T>

        T GetValue(const std::string &key, bool *success) {
            *success = false;
            latch_.RLock();

            auto pre_child = &root_;
            auto c = key.begin();
            while (c != key.end()) {
                auto cur = c++;
                auto next_node = pre_child->get()->GetChildNode(*cur);

                if (!next_node) {
                    *success = false;
                    break;
                }

                if (next_node->get()->IsEndNode() && c == key.end()) {
                    auto flag_node = dynamic_cast<TrieNodeWithValue<T> *>(next_node->get());
                    if (!flag_node) {
                        *success = false;
                        break;
                    }
                    *success = true;
                    latch_.RUnlock();
                    return flag_node->GetValue();
                }
                pre_child = next_node;
            }
            latch_.RUnlock();
            return {};
        }
    };

在这里插入图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值