CMU 15-445 2022 fall p0 p1

CMU 15-445 projects

课程Slides&Notes

15-445 & 数据库内核 - 知乎 (zhihu.com)

Project #0 - C++ Primer

完整代码
//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// p0_trie.h
//
// Identification: src/include/primer/p0_trie.h
//
// Copyright (c) 2015-2022, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#pragma once

#include <memory>
#include <stack>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

#include "common/exception.h"
#include "common/rwlatch.h"

namespace bustub {

/**
 * TrieNode is a generic container for any node in Trie.
 */
class TrieNode {
 public:

  explicit TrieNode(char key_char) {
    key_char_ = key_char;
    is_end_ = false;
    children_.clear();
  }

  TrieNode(TrieNode &&other_trie_node) noexcept {
    key_char_ = other_trie_node.key_char_;
    is_end_ = other_trie_node.is_end_;
    children_.swap(other_trie_node.children_);
  }

  virtual ~TrieNode() = default;

  bool HasChild(char key_char) const { return children_.find(key_char) != children_.end(); }

  bool HasChildren() const { return !children_.empty(); }

  bool IsEndNode() const { return is_end_; }

  char GetKeyChar() const { return key_char_; }


  std::unique_ptr<TrieNode> *InsertChildNode(char key_char, std::unique_ptr<TrieNode> &&child) {
    if (HasChild(key_char) || key_char != child->key_char_) {
      return nullptr;
    }
    children_[key_char] = std::forward<std::unique_ptr<TrieNode>>(child);
    return &children_[key_char];
  }


  std::unique_ptr<TrieNode> *GetChildNode(char key_char) {
    auto node = children_.find(key_char);
    if (node != children_.end()) {
      return &(node->second);
    }
    return nullptr;
  }


  void RemoveChildNode(char key_char) {
    auto node = children_.find(key_char);
    if (node != children_.end()) {
      children_.erase(key_char);
    }
  }


  void SetEndNode(bool is_end) { is_end_ = is_end; }

 protected:

  char key_char_;

  bool is_end_{false};

  std::unordered_map<char, std::unique_ptr<TrieNode>> children_;
};


template <typename T>
class TrieNodeWithValue : public TrieNode {
 private:
  T value_;

 public:

  TrieNodeWithValue(TrieNode &&trieNode, T value) : TrieNode(std::forward<TrieNode>(trieNode)) {
    value_ = value;
    SetEndNode(true);
  }


  TrieNodeWithValue(char key_char, T value) : TrieNode(key_char) {
    value_ = value;
    SetEndNode(true);
  }


  ~TrieNodeWithValue() override = default;


  T GetValue() const { return value_; }
};

class Trie {
 private:
  /* Root node of the trie */
  std::unique_ptr<TrieNode> root_;
  /* Read-write lock for the trie */
  ReaderWriterLatch latch_;

 public:

  Trie() {
    // auto *root = new TrieNode('\0');
    root_ = std::make_unique<TrieNode>('\0');//make_unique不是传TrieNode*,而是直接调用TrieNode的构造函数了
  }

  template <typename T>
  bool Insert(const std::string &key, T value) {
    if (key.empty()) {
      return false;
    }
    latch_.WLock();
    auto c = key.begin();
    auto pre_child = &root_;
    while (c != key.end()) {
      auto cur = c++;
      if (c == key.end()) {
        break;
      }

      if (!pre_child->get()->HasChild(*cur)) {
        pre_child = (*pre_child)->InsertChildNode(*cur, std::make_unique<TrieNode>(*cur));//调用对应拷贝构造
      } else {
        pre_child = (**pre_child).GetChildNode(*cur);
      }
    }

    c--;

    auto end_node = pre_child->get()->GetChildNode(*c);
    if (end_node != nullptr && end_node->get()->IsEndNode()) {
      latch_.WUnlock();
      return false;
    }

    if (end_node != nullptr) {
      auto new_node = new TrieNodeWithValue(std::move(**end_node), value);
      end_node->reset(new_node);
      latch_.WUnlock();
      return true;
    }

    pre_child = pre_child->get()->InsertChildNode(*c, std::make_unique<TrieNode>(*c));//调用对应拷贝构造
    auto new_node = new TrieNodeWithValue(std::move(**pre_child), value);
    pre_child->reset(new_node);
    latch_.WUnlock();
    return true;
  }


  bool Remove(const std::string &key) {
    if (key.empty()) {
      return false;
    }

    latch_.WLock();
    auto previous = &root_;
    std::vector<std::unique_ptr<TrieNode> *> vec;
    for (auto cur : key) {
      auto temp = (*previous)->GetChildNode(cur);
      if (temp == nullptr) {
        latch_.WUnlock();
        return false;
      }
      vec.emplace_back(previous);
      previous = temp;
    }
    previous->get()->SetEndNode(false);
    while (!vec.empty() && !previous->get()->IsEndNode() && !previous->get()->HasChildren()) {
      auto tmp = vec[vec.size() - 1];
      tmp->get()->RemoveChildNode((**previous).GetKeyChar());
      vec.pop_back();
      previous = tmp;
    }
    latch_.WUnlock();
    return true;
  }


  template <typename T>
  T GetValue(const std::string &key, bool *success) {
    T ret{};
    if (key.empty()) {
      *success = false;
      return ret;
    }
    latch_.RLock();
    auto pre = &root_;
    for (auto ch : key) {
      auto next = pre->get()->GetChildNode(ch);
      if (next == nullptr) {
        *success = false;
        latch_.RUnlock();
        return ret;
      }
      pre = next;
    }
    auto newnode = dynamic_cast<TrieNodeWithValue<T> *>(pre->get());
    if (newnode == nullptr) {
      *success = false;
      // ret = {};
    } else {
      *success = true;
      ret = newnode->GetValue();
    }
    latch_.RUnlock();
    return ret;
  }
};
}  // namespace bustub
C++语法参考链接
  1. typedef 与 #define 比较

    #define INTERGE int;
    unsigned INTERGE n;  //没问题
    typedef int INTERGE;
    unsigned INTERGE n;  //错误,不能在 INTERGE 前面添加 unsigned,因为INTERGE是编译时被创建的一个新的类型,并非文本替换
    
  2. 值得一看

  3. C++右值引用(std::move) - 知乎 (zhihu.com)

  4. C++11 unique_ptr智能指针详解 (biancheng.net) 现在已经有make_unique<>

  5. (51条消息) 智能指针make_unique 与make_shared 的知识介绍_aFakeProgramer的博客-CSDN博客

  6. https://www.zhihu.com/question/363686723/answer/2590214399

TrieNode的移动构造函数

注意:由于unique_ptr的特性,我们经常使用一个指针来指向智能指针!!!这样就可以防止对unique_ptr的复制了

这里对复合unique_ptr<TrieNode>unordered_map进行拷贝构造时候,需要注意智能指针的特性,对外层容器使用std::move或者使用unique_ptr<TrieNode>unordered_map都包含的swap()方法

C++ 11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝

Reference :[c++11]我理解的右值引用、移动语义和完美转发

c++中引入了右值引用移动语义,可以避免无谓的复制,提高程序性能。有点难理解,于是花时间整理一下自己的理解。

左值、右值

C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。

看见书上又将右值分为将亡值和纯右值。纯右值就是c++98标准中右值的概念,如非引用返回的函数返回的临时变量值;一些运算表达式,如1+2产生的临时变量;不跟对象关联的字面量值,如2,‘c’,true,“hello”;这些值都不能够被取地址。

而将亡值则是c++11新增的和右值引用相关的表达式,这样的表达式通常时将要移动的对象、T&&函数返回值、std::move()函数的返回值等,

不懂将亡值和纯右值的区别其实没关系,统一看作右值即可,不影响使用。

示例:

int i=0;// i是左值, 0是右值

class A {
public:
int a;
};
A getTemp()
{
return A();
}
A a = getTemp();   // a是左值  getTemp()的返回值是右值(临时变量)

左值引用、右值引用

c++98中的引用很常见了,就是给变量取了个别名,在c++11中,因为增加了右值引用(rvalue reference)的概念,所以c++98中的引用都称为了左值引用(lvalue reference)

int a = 10; 
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用

int& b = 1; //编译错误! 1是右值,不能够使用左值引用

c++11中的右值引用使用的符号是&&,如

int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {
public:
int a;
};
A getTemp()
{
return A();
}
A && a = getTemp();   //getTemp()的返回值是右值(临时变量)

getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。

注意:这里a类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值。因为可以对它取地址,而且它还有名字,是一个已经命名的右值。

所以,左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

const int & a = 1; //常量左值引用绑定 右值, 不会报错

class A {
public:
int a;
};
A getTemp()
{
return A();
}
const A & a = getTemp();   //不会报错 而 A& a 会报错

事实上,很多情况下我们用来常量左值引用的这个功能却没有意识到,如下面的例子:

#include <iostream>
using namespace std;

class Copyable {
public:
Copyable(){}
Copyable(const Copyable &o) {
  cout << "Copied" << endl;
}
};
Copyable ReturnRvalue() {
return Copyable(); //返回一个临时对象
}
void AcceptVal(Copyable a) {

}
void AcceptRef(const Copyable& a) {

}

int main() {
cout << "pass by value: " << endl;
AcceptVal(ReturnRvalue()); // 应该调用两次拷贝构造函数
cout << "pass by reference: " << endl;
AcceptRef(ReturnRvalue()); //应该只调用一次拷贝构造函数
}

当我敲完上面的例子并运行后,发现结果和我想象的完全不一样!期望AcceptVal(ReturnRvalue())需要调用两次拷贝构造函数,一次在ReturnRvalue()函数中,构造好了Copyable对象,返回的时候会调用拷贝构造函数生成一个临时对象,在调用AcceptVal()时,又会将这个对象拷贝给函数的局部变量a,一共调用了两次拷贝构造函数。而AcceptRef()的不同在于形参是常量左值引用,它能够接收一个右值,而且不需要拷贝。

而实际的结果是,不管哪种方式,一次拷贝构造函数都没有调用!

这是由于编译器默认开启了返回值优化(RVO/NRVO, RVO, Return Value Optimization 返回值优化,或者NRVO, Named Return Value Optimization)。编译器很聪明,发现在ReturnRvalue内部生成了一个对象,返回之后还需要生成一个临时对象调用拷贝构造函数,很麻烦,所以直接优化成了1个对象对象,避免拷贝,而这个临时变量又被赋值给了函数的形参,还是没必要,所以最后这三个变量都用一个变量替代了,不需要调用拷贝构造函数。

虽然各大厂家的编译器都已经都有了这个优化,但是这并不是c++标准规定的,而且不是所有的返回值都能够被优化,而这篇文章的主要讲的右值引用移动语义可以解决编译器无法解决的问题。

为了更好的观察结果,可以在编译的时候加上-fno-elide-constructors选项(关闭返回值优化)。

// g++ test.cpp -o test -fno-elide-constructors
pass by value: 
Copied
Copied //可以看到确实调用了两次拷贝构造函数
pass by reference: 
Copied

上面这个例子本意是想说明常量左值引用能够绑定一个右值,可以减少一次拷贝(使用非常量的左值引用会编译失败),但是顺便讲到了编译器的返回值优化。。编译器还是干了很多事情的,很有用,但不能过于依赖,因为你也不确定它什么时候优化了什么时候没优化。

总结一下,其中T是一个具体类型:

  1. 左值引用, 使用 T&, 只能绑定左值
  2. 右值引用, 使用 T&&, 只能绑定右值
  3. 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  4. 已命名的右值引用,编译器会认为是个左值
  5. 编译器有返回值优化,但不要过于依赖

移动构造和移动赋值

回顾一下如何用c++实现一个字符串类MyStringMyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然一个析构了,另一个也就完蛋了。

具体代码如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
//    static size_t CCtor; //统计调用拷贝构造函数的次数
public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000,因为vector的内存空间是连续的,如果一次次调用push_back()开新空间,那么需要频繁拷贝移动原空间的元素,而reserve()只是分配相应内存空间,并没有构建实际对象,只有在push_back操作时才会在vecStr已经分配的空间中创建对象
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << MyString::CCtor << endl;
}

代码看起来挺不错,却发现执行了1000次拷贝构造函数,如果MyString("hello")构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString("hello")只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。而C++11新增加的移动语义就能够做到这一点。

要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数。

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
    static size_t MCtor; //统计调用移动构造函数的次数
    static size_t CAsgn; //统计调用拷贝赋值函数的次数
    static size_t MAsgn; //统计调用移动赋值函数的次数

public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 移动构造函数
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr; //不再指向之前的资源了
   }

   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 移动赋值函数 =号重载
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str) // 避免自我赋值!!
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr; //不再指向之前的资源了
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 结果
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

可以看到,移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString("hello")是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不用奇怪为什么可以抢别人的资源,临时对象的资源不好好利用也是浪费,因为生命周期本来就是很短,在你执行完这个表达式之后,它就毁灭了,充分利用资源,才能很高效。

对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr.push_back(tmp); //调用的是拷贝构造函数
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;

    cout << endl;
    MyString::CCtor = 0;
    MyString::MCtor = 0;
    MyString::CAsgn = 0;
    MyString::MAsgn = 0;
    vector<MyString> vecStr2;
    vecStr2.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        MyString tmp("hello");
        vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 运行结果
CCtor = 1000
MCtor = 0
CAsgn = 0
MAsgn = 0

CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

下面再举几个例子:

MyString str1("hello"); //调用构造函数
MyString str2("world"); //调用构造函数
MyString str3(str1); //调用拷贝构造函数
MyString str4(std::move(str1)); // 调用移动构造函数、
//    cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要使用
//注意:虽然str1中的m_dat已经称为了空,但是str1这个对象还活着,知道出了它的作用域才会析构!而不是move完了立刻析构
MyString str5;
str5 = str2; //调用拷贝赋值函数
MyString str6;
str6 = std::move(str2); // str2的内容也失效了,不要再使用

需要注意一下几点:

  1. str6 = std::move(str2),虽然将str2的资源给了str6,但是str2并没有立刻析构,只有在str2离开了自己的作用域的时候才会析构,所以,如果继续使用str2m_data变量,可能会发生意想不到的错误。
  2. 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!
  3. c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

universal references(通用引用)

当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:

template<typename T>
void f( T&& param){
    
}
f(10);  //10是右值
int x = 10; //
f(x); //x是左值

如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。

注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references

例如:

template<typename T>
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references

template<typename T>
class Test {
  Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用  
};

void f(Test&& param); //右值引用

//复杂一点
template<typename T>
void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
//已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用

template<typename T>
void f(const T&& param); //右值引用
// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效

所以最终还是要看T被推导成什么类型,如果T被推导成了string,那么T&&就是string&&,是个右值引用,如果T被推导为string&,就会发生类似string& &&的情况,对于这种情况,c++11增加了引用折叠的规则,总结如下:

  1. 所有的右值引用叠加到右值引用上仍然使一个右值引用。
  2. 所有的其他引用类型之间的叠加都将变成左值引用。

如上面的T& &&其实就被折叠成了个string &,是一个左值引用。

#include <iostream>
#include <type_traits>
#include <string>
using namespace std;

template<typename T>
void f(T&& param){
    if (std::is_same<string, T>::value)
        std::cout << "string" << std::endl;
    else if (std::is_same<string&, T>::value)
        std::cout << "string&" << std::endl;
    else if (std::is_same<string&&, T>::value)
        std::cout << "string&&" << std::endl;
    else if (std::is_same<int, T>::value)
        std::cout << "int" << std::endl;
    else if (std::is_same<int&, T>::value)
        std::cout << "int&" << std::endl;
    else if (std::is_same<int&&, T>::value)
        std::cout << "int&&" << std::endl;
    else
        std::cout << "unknown" << std::endl;
}

int main()
{
    int x = 1;
    f(1); // 参数是右值 T推导成了int, 所以是int&& param, 右值引用
    f(x); // 参数是左值 T推导成了int&, 所以是int&&& param, 折叠成 int&,左值引用
    int && a = 2;
    f(a); //虽然a是右值引用,但它还是一个左值, T推导成了int&
    string str = "hello";
    f(str); //参数是左值 T推导成了string&
    f(string("hello")); //参数是右值, T推导成了string
    f(std::move(str));//参数是右值, T推导成了string
}

所以,归纳一下, 传递左值进去,就是左值引用,传递右值进去,就是右值引用。如它的名字,这种类型确实很"通用",下面要讲的完美转发,就利用了这个特性。

完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

void process(int& i){
    cout << "process(int&):" << i << endl;
}
void process(int&& i){
    cout << "process(int&&):" << i << endl;
}

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(i);
}

int main()
{
    int a = 0;
    process(a); //a被视为左值 process(int&):0
    process(1); //1被视为右值 process(int&&):1
    process(move(a)); //强制将a由左值改为右值 process(int&&):0
    myforward(2);  //右值经过forward函数转交给process函数,却称为了一个左值,
    //原因是该右值有了名字  所以是 process(int&):2
    myforward(move(a));  // 同上,在转发的时候右值变成了左值  process(int&):0
    // forward(a) // 错误用法,右值引用不接受左值
}

上面的例子就是不完美转发,而c++中提供了一个std::forward()模板函数解决这个问题。将上面的myforward()函数简单改写一下:

void myforward(int&& i){
    cout << "myforward(int&&):" << i << endl;
    process(std::forward<int>(i));
}

myforward(2); // process(int&&):2

上面修改过后还是不完美转发,myforward()函数能够将右值转发过去,但是并不能够转发左值,解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。例子如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

void RunCode(int &&m) {
    cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
    cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
    cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
    cout << "const lvalue ref" << endl;
}

// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
    RunCode(forward<T> (t));
}

template<typename T>
void notPerfectForward(T && t) {
    RunCode(t);
}

int main()
{
    int a = 0;
    int b = 0;
    const int c = 0;
    const int d = 0;

    notPerfectForward(a); // lvalue ref
    notPerfectForward(move(b)); // lvalue ref
    notPerfectForward(c); // const lvalue ref
    notPerfectForward(move(d)); // const lvalue ref

    cout << endl;
    perfectForward(a); // lvalue ref
    perfectForward(move(b)); // rvalue ref
    perfectForward(c); // const lvalue ref
    perfectForward(move(d)); // const rvalue ref
}

上面的代码测试结果表明,在universal referencesstd::forward的合作下,能够完美的转发这4种类型。

emplace_back减少内存拷贝和移动

我们之前使用vector一般都喜欢用push_back(),由上文可知容易发生无谓的拷贝,解决办法是为自己的类增加移动拷贝和赋值函数,但其实还有更简单的办法!就是使用emplace_back()替换push_back(),如下面的例子:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class A {
public:
    A(int i){
//        cout << "A()" << endl;
        str = to_string(i);
    }
    ~A(){}
    A(const A& other): str(other.str){
        cout << "A&" << endl;
    }

public:
    string str;
};

int main()
{
    vector<A> vec;
    vec.reserve(10);
    for(int i=0;i<10;i++){
        vec.push_back(A(i)); //调用了10次拷贝构造函数
//        vec.emplace_back(i);  //一次拷贝构造函数都没有调用过
    }
    for(int i=0;i<10;i++)
        cout << vec[i].str << endl;
}

可以看到效果是明显的,虽然没有测试时间,但是确实可以减少拷贝。emplace_back()可以原地直接通过构造函数的参数构造对象,但前提是要有对应的构造函数

对于mapset,可以使用emplace()。基本上emplace_back()对应push_bakc(), emplce()对应insert()

移动语义对swap()函数的影响也很大,之前实现swap可能需要三次内存拷贝,而有了移动语义后,就可以实现高性能的交换函数了。

template <typename T>
void swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

如果T是可移动的,那么整个操作会很高效,如果不可移动,那么就和普通的交换函数是一样的,不会发生什么错误,很安全。

总结

  • 由两种值类型,左值和右值。
  • 有三种引用类型,左值引用、右值引用和通用引用。左值引用只能绑定左值,右值引用只能绑定右值,通用引用由初始化时绑定的值的类型确定。
  • 左值和右值是独立于他们的类型的,右值引用可能是左值可能是右值,如果这个右值引用已经被命名了,他就是左值。
  • 引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
  • 移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值函数。
  • std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作。
  • std::forward()universal references通用引用共同实现完美转发。
  • empalce_back()替换push_back()增加性能。
std::unordered_map<char, std::unique_ptr<TrieNode>> children_;

TrieNode(TrieNode &&other_trie_node) noexcept {
    key_char_ = other_trie_node.key_char_;
    is_end_ = other_trie_node.is_end_;
    children_.swap(other_trie_node.children_);//children_ = std::move(other_trie_node.children_);
  }
子类TrieNodeWithValue转发构造函数

对于TrieNode继承派生出的子类TrieNodeWithValueTrieNode(trieNode) 不会调用基类 TrieNode 的移动构造函数。应该使用成员初始化列表将 trieNode 转发(forward)给基类构造函数进行移动构造。

TrieNodeWithValueTrieNode 的子类,构造子类时,如果没有手动在子类的构造函数中构造父类,就会调用父类的默认构造函数,而代码中父类是没有默认构造函数的,所以需要手动在子类中构造父类。

需要使用 std::forward<TrieNode> 转发右值引用。

template <typename T>
class TrieNodeWithValue : public TrieNode {
 private:
  /* Value held by this trie node. */
  T value_;

 public:
  /**
   * TODO(P0): Add implementation
   *
   * @brief Construct a new TrieNodeWithValue object from a TrieNode object and specify its value.
   * This is used when a non-terminal TrieNode is converted to terminal TrieNodeWithValue.
   *
   * The children_ map of TrieNode should be moved to the new TrieNodeWithValue object.
   * Since it contains unique pointers, the first parameter is a rvalue reference.
   *
   * You should:
   * 1) invoke TrieNode's move constructor to move data from TrieNode to
   * TrieNodeWithValue.
   * 2) set value_ member variable of this node to parameter `value`.
   * 3) set is_end_ to true
   *
   * @param trieNode TrieNode whose data is to be moved to TrieNodeWithValue
   * @param value
   */
  TrieNodeWithValue(TrieNode &&trieNode, T value) : TrieNode(std::move(trieNode)) {
    value_ = value;
    SetEndNode(true);
  }//由于右值引用是左值,而转发需要保持原传入参数的特性,因此这里需要std::move将trieNode转化为右值,或者用std::forward
  
  //错误写法
  TrieNodeWithValue(TrieNode &&trieNode, T value) {
    TrieNode(trieNode);
    value_ = value;
    is_end_ = true;
  }
auto关键字

auto关键字的用法,以及如何初始化一个已经声明的unique_ptr智能指针:首先用一个普通指针(auto)来在堆上new相应对象,然后通过智能指针的reset()方法获得该对象的唯一控制权

reset(p):其中 p 表示一个普通指针,如果 pnullptrunique_ptr 也变成空指针;反之,则该函数会释放当前 unique_ptr 指针指向的堆内存(如果有),然后获取 p 所指堆内存的所有权(然后 p 变为 nullptr)。

class Trie {
 private:
  /* Root node of the trie */
  std::unique_ptr<TrieNode> root_;
  /* Read-write lock for the trie */
  ReaderWriterLatch latch_;

 public:
  /**
   * TODO(P0): Add implementation
   *
   * @brief Construct a new Trie object. Initialize the root node with '\0'
   * character.
   */
//上下效果好像一样,都能通过测试
Trie() {
    auto *root = new TrieNode('\0');
    root_.reset(root);
  }
  
Trie() {
    auto root = new TrieNode('\0');
    root_.reset(root);
  }

Trie的实现,初始构造一个参数为'\0'的TrieNode根节点

class Trie {
 private:
  /* Root node of the trie */
  std::unique_ptr<TrieNode> root_;
  /* Read-write lock for the trie */
  ReaderWriterLatch latch_;

 public:
  /**
   * TODO(P0): Add implementation
   *
   * @brief Construct a new Trie object. Initialize the root node with '\0'
   * character.
   */
  Trie() {
    auto *root = new TrieNode('\0');
    root_.reset(root);
  }

这里也可以用make_unique直接构造

root_ = make_unique<TrieNode>('\0');
关键实现:Insert()

首先注意到这句话

If the key already exists, return false. Duplicated keys are not allowed and you should never overwrite value of an existing key.

那么提出问题:如何在一个字典树中判断一个string key是否已经存在?

很简单,逐位比较即可,如果中间存在不一样的字符位,那么必然会产生InsertChildNode的过程,而从第一个不一样的(即在原Trie中不存在)的字符开始,该字符串接下来的每一个字符都会是InsertChildNode

注意 When you reach the ending character of a key 要单独处理,此时只要

auto end_node = pre_child->get()->GetChildNode(*cur);
end_node != nullptr;

即说明当前待插入的key字符串已经在书中全部存在,有可能是其他串的前缀,也有可能就是这个一模一样的串,即前缀匹配

那么这时只需要看是否endNode就能判断情况2/3

end_node == nullptr

说明Trie中必与当前key不能匹配,情况1

比如你需要插入“apple”,而当前Trie中已经有串"apple"或者“appletree”,那么当cur指向key.end()-1也就是’e’的迭代器时,循环结束,而此时**GetChildNode()返回必不为nullptr**,因为完全匹配了

反之原因就是上文加粗部分,如果串中任意位置有不匹配的部分,比如你需要插入"apple",而Trie中存在“abple”,从第二个字符’b’开始不匹配,循环体随即执行pre_child = pre_child->get()->InsertChildNode(*cur, std::make_unique<TrieNode>(*cur))开辟了一个符号为’p’新的树枝,然后pre_child进入了这新的树枝,导致接下来"ple"部分在’p’树枝上都是不存在的,需要一直InsertChildNode

同理判断最后一个节点的上一个节点是否有该节点儿子时,如果有则说明完全前缀匹配了,如果没有表示该串不匹配

注意一些细节:

  • 考虑到节点不容易进行复制,我们可以直接新建节点或转移节点,使用reset函数进行更新

  • auto new_node = new TrieNodeWithValue(std::move(**end_node), value);
    

    其中移动构造将end_node下所有堆上资源也即children_移动给new_node,因此不用担心变更为TrieNodeWithValue后断链,丢失子女链接

  • end_node->reset(new_node);
    

    其中end_nodeunique_pre<TrieNode>*类型,内含指针是TrieNode*的,而new_node是子类指针TrieNodewithvalue*的,但是可以直接用unique_ptr.reset方法,记住这一特性:

    包含父类指针的unique_ptr可以直接对子类指针使用reset()重写内存

  • 上下合在一起看构建新的TrieNodeWithValue节点逻辑,即在父亲节点构造/已存在对应普通儿子节点TrieNode后,赋值给end_node(此时父亲节点的children_中已经存在了end_node的值,这个值是不变的,来保证不断链),然后将其资源移动给新构造的TrieNodewithvalue,再将end_node.reset() 获得资源,整个过程下来父亲节点的children中end_node的值是不变的,只是转移了资源和类属性

    注意是end_node调用reset()方法,夺回资源,因此需要先进入孩子节点end_node,保证不断链

    pre_child/end_node值是不变的,只对其所指向的地址中存放的unique_ptr中的资源进行操作

  • make_unique<type> 获得一次性的右值unique_ptr用于构造函数

  • unique_ptr.get()获得其中包含的原指针,而**end_node是获得其中指针指向的TreeNode对象,区分二者

    其中

    auto end_node = pre_child->get()->GetChildNode(*cur);
    

    等价于

    auto end_node = *(pre_child)->GetChildNode(*cur);
    
  /**
   * TODO(P0): Add implementation
   *
   * @brief Insert key-value pair into the trie.
   *
   * If the key is an empty string, return false immediately.
   *
   * If the key already exists, return false. Duplicated keys are not allowed and
   * you should never overwrite value of an existing key.
   *
   * When you reach the ending character of a key:
   * 1. If TrieNode with this ending character does not exist, create new TrieNodeWithValue
   * and add it to parent node's children_ map.
   * 2. If the terminal node is a TrieNode, then convert it into TrieNodeWithValue by
   * invoking the appropriate constructor.
   * 3. If it is already a TrieNodeWithValue,
   * then insertion fails and returns false. Do not overwrite existing data with new data.
   *
   * You can quickly check whether a TrieNode pointer holds TrieNode or TrieNodeWithValue
   * by checking the is_end_ flag. If is_end_ == false, then it points to TrieNode. If
   * is_end_ == true, it points to TrieNodeWithValue.
   *
   * @param key Key used to traverse the trie and find the correct node
   * @param value Value to be inserted
   * @return True if insertion succeeds, false if the key already exists
   */
  template <typename T>
  bool Insert(const std::string &key, T value) {
    if (key.empty()) {
      return false;
    }
    latch_.WLock();
    auto endstr = key.end()-1;
    auto cur = key.begin();//cur作为字符串逐位比较的迭代器
    auto pre_child = &root_;//pre_child作为Trie迭代器,从根开始与字符串逐位比较,看当前cur代表字符是否在children_中存在
    while (cur != endstr) {//字符串的结尾字符,需要单独处理,其余中间字符要么Insert要么直接往后迭代
      if (!pre_child->get()->HasChild(*cur)) {//unique_ptr.get()获得原指针,这里也就是TreeNode*
        pre_child = pre_child->get()->InsertChildNode(*cur, std::make_unique<TrieNode>(*cur));
      } else {
        pre_child = pre_child->get()->GetChildNode(*cur);
      }
      cur++;
    }
    
    //When you reach the ending character of a key:
    auto end_node = pre_child->get()->GetChildNode(*cur);
      
    //If it is already a TrieNodeWithValue, then insertion fails and returns false. Do not overwrite existing data with new data.
    if (end_node != nullptr && end_node->get()->IsEndNode()) {//看是否endNode就能判断情况2/3
      latch_.WUnlock();
      return false;
    }

    if (end_node != nullptr) {//情况2,需要把原有的普通TrieNode转换为新的endNode也即TrieNodewithvalue
      auto new_node = new TrieNodeWithValue(std::move(**end_node), value);//这里new_node类型为TrieNodeWithValue*
      end_node->reset(new_node);//这里注意细节,父类初始化的unique_ptr直接对继承类指针调用reset()
      latch_.WUnlock();
      return true;
    }//整个过程下来父亲节点的children_中end_node的值是不变的,只是转移了资源和类属性

    pre_child = pre_child->get()->InsertChildNode(*cur, std::make_unique<TrieNode>(*cur));//这里同理,需要先进入创建的孩子节点,再进行节点转换,保证不断链
    auto new_node = new TrieNodeWithValue(std::move(**pre_child), value);
    pre_child->reset(new_node);//pre_child值是不变的,只对其所指向的地址中存放的unique_ptr中的资源进行操作
    latch_.WUnlock();
    return true;
  }

节点转换部分也可以这么写,

注意过程中是对curr也就是unique_ptr*进行操作,而父亲节点中children_存储的是unique_ptr,能更好看出防止断链而操作指针的思想

(*curr) = std::make_unique<TrieNodeWithValue<T>>(std::move(*(*curr)), value);

其中

  • 上面代码的end_node和这里的curr同为unique_ptr*类型,可解引用方式而言,这里是*curr而上文为end_node->,为什么是这样?

    • -> 运算符用于访问指针所指对象的成员,而 * 运算符用于解引用指针,以访问它所指向的对象
    • 因为这里直接将unique_ptr看作整个对象进行赋值,因此直接*取对象,而上文将unique_ptr看作进行操作的类对象,因此用->调用类函数reset()
    • *p->mem 等价于 (*p).mem
  • 注意模板类作为签名的写法:TrieNodeWithValue<T>,需要用尖括号填入template <typename T>中的T

  • make_unique函数实现如下

    template<typename T, typename... Ts>
    std::unique_ptr<T> make_unique(Ts&&... params)
    {
        return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
    }
    

    make_unique完美传递了参数给对象的构造函数,从一个原始指针构造出一个std::unique_ptr,返回创建的std::unique_ptr

Remove()

用栈保留遍历的路径,std:tuple记录 字符 与其 父节点 的映射关系(这里面的节点都是用unique_ptr*的形式表示的)

改成直接用数组当栈,记录父亲节点

参考链接

/**
   * TODO(P0): Add implementation
   *
   * @brief Remove key value pair from the trie.
   * This function should also remove nodes that are no longer part of another
   * key. If key is empty or not found, return false.
   *
   * You should:
   * 1) Find the terminal node for the given key.
   * 2) If this terminal node does not have any children, remove it from its
   * parent's children_ map.
   * 3) Recursively remove nodes that have no children and are not terminal node
   * of another key.
   *
   * @param key Key used to traverse the trie and find the correct node
   * @return True if the key exists and is removed, false otherwise
   */
 bool Remove(const std::string &key) {
    if (key.empty()) {
      return false;
    }

    latch_.WLock();
    auto previous = &root_;
    std::vector<std::unique_ptr<TrieNode> *> vec;
    for (auto cur : key) {
      auto temp = (*previous)->GetChildNode(cur);
      if (temp == nullptr) {
        latch_.WUnlock();
        return false;
      }
      vec.emplace_back(previous);
      previous = temp;
    }
    previous->get()->SetEndNode(false);//如果这个串在字典树内,先把尾节点设置成非endnode,无论其有无儿子
    //然后把没有孩子,并且非尾巴节点删除
    while (!vec.empty() && !previous->get()->IsEndNode() && !previous->get()->HasChildren()) {
      auto tmp = vec[vec.size() - 1];
      tmp->get()->RemoveChildNode((**previous).GetKeyChar());
      vec.pop_back();
      previous = tmp;
    }
    latch_.WUnlock();
    return true;
  }

递归版本

remove_innner 的返回值表示传入节点是否可以删除,可以删除的条件是该节点无子节点且非尾节点。函数内判断当前节点的子节点是否可以删除,并返回当前节点是否可以删除。出口是递归到了传入 key 的尾节点,取消尾节点标记,并返回是否可以删除。该函数调用前还需要判断一下 key 是否存在。

bool remove_inner(const std::string &key, size_t i, std::unique_ptr<TrieNode> *curr, bool *success) {
  if(curr == nullptr) return false;
  if (i == key.size()) {
    *success = true; // Remove 的返回值,表示成功删除
    (*curr)->SetEndNode(false);
    return !(*curr)->HasChildren() && !(*curr)->IsEndNode();
  }

  bool can_remove = remove_inner(key, i + 1, (*curr)->GetChildNode(key[i]), success);

  if (can_remove) {
    (*curr)->RemoveChildNode(key[i]);
  }
  return !(*curr)->HasChildren() && !(*curr)->IsEndNode();
}
GetValue()

参考链接

  1. 返回值,题中有不返回值的情况,但是函数为模板类,必须返回T类型的初始化变量,以下三种方式等价

    return {};
    return T{};
    return T();
    //https://blog.csdn.net/zqxf123456789/article/details/107128067
    //https://blog.csdn.net/u012011079/article/details/114080919
    
  2. dynamic_cast<模板类>后判断是否空指针,用来判断是否指定的模板类型,看注释写即可

/**
   * TODO(P0): Add implementation
   *
   * @brief Get the corresponding value of type T given its key.
   * If key is empty, set success to false.
   * If key does not exist in trie, set success to false.
   * If the given type T is not the same as the value type stored in TrieNodeWithValue
   * (ie. GetValue<int> is called but terminal node holds std::string),
   * set success to false.
   *
   * To check whether the two types are the same, dynamic_cast
   * the terminal TrieNode to TrieNodeWithValue<T>. If the casted result
   * is not nullptr, then type T is the correct type.
   *
   * @param key Key used to traverse the trie and find the correct node
   * @param success Whether GetValue is successful or not
   * @return Value of type T if type matches
   */
  template <typename T>
  T GetValue(const std::string &key, bool *success) {
    T ret;
    if (key.empty()) {
      *success = false;
      return {};//return T{};
    }
    latch_.RLock();
    auto pre = &root_;
    for (auto ch : key) {
      auto next = pre->get()->GetChildNode(ch);
      if (next == nullptr) {
        *success = false;
        latch_.RUnlock();
        return {};
      }
      pre = next;
    }
    auto newnode = dynamic_cast<TrieNodeWithValue<T> *>(pre->get());
    if (newnode == nullptr) {
      *success = false;
      ret = {};
    } else {
      *success = true;
      ret = newnode->GetValue();
    }
    latch_.RUnlock();
    return ret;
  }

Project #1 - Buffer Pool

(54条消息) 【CMU15-445数据库】bustub Project #1:Buffer Pool_bustub project1_Altair_Alpha_的博客-CSDN博客

Task #1 - Extendible Hash Table
参考链接
  1. Extendible Hashing (Dynamic approach to DBMS) - GeeksforGeeks

  2. CMU15445 2022 Project1-Buffer Pool Manager攻略 - 知乎 (zhihu.com)

  3. C++ 并发编程(二):Mutex(互斥锁) - 止于至善 - SegmentFault 思否

  4. C++ 中的 Lambda 表达式 | Microsoft Learn

  5. Lambda 表达式语法 | Microsoft Learn

  6. Lambda 表达式的示例 | Microsoft Learn

  7. (51条消息) 智能指针make_unique 与make_shared 的知识介绍_aFakeProgramer的博客-CSDN博客

  8. (54条消息) 《Effective Modern C++》学习笔记 - Item 42: 考虑使用emplace代替插入_emplace重复元素_Altair_Alpha_的博客-CSDN博客

完整代码
//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// extendible_hash_table.cpp
//
// Identification: src/container/hash/extendible_hash_table.cpp
//
// Copyright (c) 2022, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#include <cassert>
#include <cstdlib>
#include <functional>
#include <list>
#include <utility>

#include "container/hash/extendible_hash_table.h"
#include "storage/page/page.h"

namespace bustub {

template <typename K, typename V>
ExtendibleHashTable<K, V>::ExtendibleHashTable(size_t bucket_size)
    : global_depth_(1), bucket_size_(bucket_size), num_buckets_(2) {
  dir_.push_back(std::make_shared<Bucket>(bucket_size, 1));
  dir_.push_back(std::make_shared<Bucket>(bucket_size, 1));
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::IndexOf(const K &key) -> size_t {
  int mask = (1 << global_depth_) - 1;
  return std::hash<K>()(key) & mask;
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetGlobalDepth() const -> int {
  std::scoped_lock<std::mutex> lock(latch_);
  return GetGlobalDepthInternal();
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetGlobalDepthInternal() const -> int {
  return global_depth_;
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetLocalDepth(int dir_index) const -> int {
  std::scoped_lock<std::mutex> lock(latch_);
  return GetLocalDepthInternal(dir_index);
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetLocalDepthInternal(int dir_index) const -> int {
  return dir_[dir_index]->GetDepth();
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetNumBuckets() const -> int {
  std::scoped_lock<std::mutex> lock(latch_);
  return GetNumBucketsInternal();
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::GetNumBucketsInternal() const -> int {
  return num_buckets_;
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Find(const K &key, V &value) -> bool {
  std::scoped_lock<std::mutex> lock(latch_);

  auto index = IndexOf(key);
  auto target_bucket = dir_[index];

  return target_bucket->Find(key, value);
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Remove(const K &key) -> bool {
  std::scoped_lock<std::mutex> lock(latch_);

  auto index = IndexOf(key);
  auto target_bucket = dir_[index];

  return target_bucket->Remove(key);
}

template <typename K, typename V>
void ExtendibleHashTable<K, V>::Insert(const K &key, const V &value) {
  std::scoped_lock<std::mutex> lock(latch_);

  while (dir_[IndexOf(key)]->IsFull()) {//看对应的桶满了没
    auto index = IndexOf(key);
    auto target_bucket = dir_[index];

    if (target_bucket->GetDepth() == global_depth_) {
      global_depth_++;
      auto oldcapacity = dir_.size();

      // double the size of the directory.
      dir_.resize(oldcapacity << 1); //vector直接用resize()
      for (size_t i = 0; i < oldcapacity; i++) {
        dir_[i + oldcapacity] = dir_[i];
      }
    }

    // Increment the local depth of the bucket.
    auto bucket_0 = std::make_shared<Bucket>(bucket_size_, target_bucket->GetDepth() + 1);
    auto bucket_1 = std::make_shared<Bucket>(bucket_size_, target_bucket->GetDepth() + 1);

    size_t mask = 1 << target_bucket->GetDepth();//只有这一步是用到桶的本地深度的,也就是桶自身分裂,rehash其中元素的时候,需要用桶的深度来确定                                                                                                                          rehash位

    //下面是两个与mask做&的for循环,第一个根据上面得出来的hash位,rehash旧桶中的元素的key入两个新桶中,后面一个是对「目录下标指向的桶」重新分配,本质上其实是同一件事情,都是对新分配的标志位hash位进行的判断
      
    // Split the bucket and redistribute directory pointers & the kv pairs in the bucket.
    for (const auto &item : target_bucket->GetItems()) {
      if ((std::hash<K>()(item.first) & mask) == 0U) {
        bucket_0->Insert(item.first, item.second);
      } else {
        bucket_1->Insert(item.first, item.second);
      }
    }

    num_buckets_++;

    // cautious size_t,做位运算需要用unsigned数,即size_t
    for (size_t i = 0; i < dir_.size(); ++i) {
      if (dir_[i] == target_bucket) {
        dir_[i] = ((i & mask) == 0U) ? bucket_0 : bucket_1;
      }
    }
  }//while end
  auto target_bucket = dir_[IndexOf(key)];
  for (auto &item : target_bucket->GetItems()) {
    if (item.first == key) {
      item.second = value;
      return;
    }
  }
  target_bucket->Insert(key, value);
}

//===--------------------------------------------------------------------===//
// Bucket
//===--------------------------------------------------------------------===//
template <typename K, typename V>
ExtendibleHashTable<K, V>::Bucket::Bucket(size_t array_size, int depth) : size_(array_size), depth_(depth) {}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Find(const K &key, V &value) -> bool {
    for(const auto &[k,v]:list_){//遍历键值对时使用 structured binding,避免还要写 pair.first 和 pair.second
                                 //https://blog.csdn.net/hubing_hust/article/details/128667994
        if(key == k){
            value = v;
            return true;
	}
    return false;
}
  /*return std::any_of(list_.begin(), list_.end(), [&key, &value](const auto &item) {
    if (item.first == key) {
      value = item.second;
      return true;
    }
    return false;
  });*/
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Remove(const K &key) -> bool {
  return std::any_of(list_.begin(), list_.end(), [&key, this](const auto &item) {
    if (item.first == key) {
      this->list_.remove(item);
      return true;
    }
    return false;
  });
}

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Insert(const K &key, const V &value) -> bool {
  if (IsFull()) {
    return false;
  }
  list_.emplace_back(key, value);  // list_.push_back(std::make_pair(key,value));
  return true;
}

template class ExtendibleHashTable<page_id_t, Page *>;
template class ExtendibleHashTable<Page *, std::list<Page *>::iterator>;
template class ExtendibleHashTable<int, int>;
// test purpose
template class ExtendibleHashTable<int, std::string>;
template class ExtendibleHashTable<int, std::list<int>::iterator>;

}  // namespace bustub

设计思路

拉链法实现的哈希表当某个哈希值对应的(即存在于同一个“桶”中,Bucket)元素特别多时,查找的时间复杂度由 O ( 1 ) 退化为遍历的 O ( n ) 。举个极端的例子,如果哈希函数是不管输入是什么都映射为 0,那么就和在第 0 位存储一个链表无异。如何设计散布更加均匀的哈希函数是优化的另一个方向,而另一种方法是当检测到某个桶中的元素过多时对表进行扩展。扩展最简单的做法是直接将哈希表的长度(桶数)翻倍,再将哈希函数的值域由 [ 0 , n ) 改为 [ 0 , 2 n ) (这点很容易实现,因为大部分哈希函数本身就是先算出某个值,然后对 n 取余数),然后对所有存储的元素重新算一次哈希值分布到不同的桶中。

这种方法的缺点很明显:如果哈希表中已经存储了大量的元素,因为要对所有元素重算哈希值,扩展的过程会有巨大的计算量,导致一次突发的大延迟。实际上,进行扩展时,可能仅仅是某一个桶出现了拉链很长的状况,其它桶的余量还很充足。于是,出现了可扩展哈希表(Extendible Hash Table)的方案,其将哈希得到的下标与桶改为非一对一映射,并引入全局深度(Global Depth)和局部深度(Local Depth)的概念,实现扩展时只需对达到容量的那一个桶进行分裂,解决了以上问题。

  • 注意stl中的vector作为目录结构list作为目录项中存放的链表结构,也就是实际上的桶,而链表中使用pair存储键值对
互斥锁

见上面参考链接3,对桶的任何读取操作(包括GetGlobalDepth()GetLocalDepth()GetLocalDepth()这三个读取了私有成员而没读取桶的?)都要加上

std::scoped_lock<std::mutex> lock(latch_);
push_backemplace_back

[《Effective Modern C++》学习笔记 - Item 42: 考虑使用emplace代替插入_Altair_Alpha_的博客-CSDN博客]

注意两者区别,push_back需要插入一个完整对象,而emplace_back只需要传参数,自动调用内部对象构造函数,不需要自己手动构造对象

//std::list<std::pair<K, V>> list_;
auto Insert(const K &key, const V &value) -> bool {
      if (IsFull()) return false;
      list_.push_back(std::make_pair(key, value));  // list_.emplace_back(key,value);
      return true;
    }

同样效果的还有shared_ptr<>make_shared<>unique_ptr<>

push_back方法会将一个已经存在的对象复制一份并添加到容器中,因此需要先创建一个对象然后再将其添加到容器中。

emplace_back方法则是在容器的末尾直接构造一个新对象,而不是先创建一个对象再将其添加到容器中。也就是说,它在容器的末尾直接构造一个新元素,不需要先创建一个对象再将其添加到容器中,因此可以更加高效。

需要注意的是,emplace_back方法只能使用对象的构造函数来构造新的元素,因此我们需要保证构造函数的正确性。

ExtendibleHashTable类的构造函数

按照定义

template <typename K, typename V>
ExtendibleHashTable<K, V>::ExtendibleHashTable(size_t bucket_size)
    : global_depth_(1), bucket_size_(bucket_size), num_buckets_(2) {
  dir_.push_back(std::make_shared<Bucket>(bucket_size, 1));
  dir_.push_back(std::make_shared<Bucket>(bucket_size, 1));
}

这里为什么不能写成

dir_.emplace_back(bucket_size, 1);

dir_是一个std::vector<std::shared_ptr<Bucket>>类型的容器,其中存储的元素是std::shared_ptr<Bucket>类型的智能指针,而不是直接存储Bucket对象。因此,如果直接使用dir_.emplace_back(bucket_size, 1)来添加元素,则会产生编译错误,因为std::shared_ptr<Bucket>类没有相应的构造函数来接受bucket_size1这两个参数。

在这种情况下,应该使用std::make_shared函数来创建一个新的std::shared_ptr<Bucket>对象,并将其添加到容器中。std::make_shared函数可以接受任意数量的参数,并将它们传递给Bucket类的构造函数来创建一个新的对象,然后返回一个指向该对象的智能指针。因此,正确的写法是使用std::make_shared函数来创建新的std::shared_ptr<Bucket>对象,如下所示:

dir_.emplace_back(std::make_shared<Bucket>(bucket_size, 1));
std::any_of()

代码格式检查要求使用std::any_of()来实现桶对象的内置函数,比如

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Remove(const K &key) -> bool {
  return std::any_of(list_.begin(), list_.end(), [&key, this](const auto &item) {
    if (item.first == key) {
      this->list_.remove(item);
      return true;
    }
    return false;
  });
}

其中std::any_of() 是一个STL算法,用于判断一个容器中是否存在满足指定条件的元素。它接受三个参数:容器开始和结束迭代器以及一个谓词(可调用对象),用于指定要匹配的条件

list_.begin()list_.end() 分别是 list_ 容器的开始和结束迭代器。[&key,this](const auto& item) { ... } 是一个lambda表达式,它将 key 作为捕获值,并检查键是否等于 keystd::any_of() 将迭代容器中的每个元素,并应用提供的lambda表达式,返回一个 bool 值,表示是否有满足条件的元素。

要在类成员函数体中使用 Lambda 表达式,请将 this 指针传递给 capture 子句,以提供对封闭类的成员函数和数据成员的访问权限。

关于lambda表达式见参考链接

已更新,不用std::any_of()也可以

为什么迭代器应该使用 auto item = list_.begin();,而不是 auto &item = list_.begin();

因为您在遍历过程中尝试删除元素,而这会导致迭代器失效。您在使用 list_.remove(*item) 删除元素后,item 迭代器已经无效,因此在继续使用它会导致未定义行为。

为了正确地删除元素,您可以使用 list_.erase(item),这会删除当前迭代器指向的元素并返回下一个有效的迭代器。这样就不会出现迭代器失效的问题

template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Find(const K &key, V &value) -> bool {
  for (const auto &item : list_) {
    if (item.first == key) {
      value = item.second;
      return true;
    }
  }
  return false;
}

//这里使用迭代器代替kv遍历即可,auto item= list_.begin()不能写作auto &item= list_.begin(),因为这里对item做了更改?
template <typename K, typename V>
auto ExtendibleHashTable<K, V>::Bucket::Remove(const K &key) -> bool {
  for (auto item = list_.begin(); item != list_.end(); item++) {
    if ((*item).first == key) {
      list_.remove(*item);
      return true;
    }
  }
  return false;
}
哈希映射与IndexOf()

对于任意类型K,需要将其映射为二进制,进而取后global_depth_位,然后在索引表(目录)中找到对应桶

使用std::hash<K>()方法,chatgpt解释如下

std::hash<K>() 是一个函数对象,用于对类型 K 进行哈希计算。通过调用 std::hash<K>() 可以获取到这个函数对象的一个实例。

当我们调用 std::hash<K>()(key) 时,会将键 key 作为参数传递给哈希函数对象,该对象对其进行哈希计算,并返回一个「无符号整数」类型的哈希值

mask 是一个掩码,它是一个二进制全 1 的数,用于限制哈希值的范围。通过使用位与运算符 & 将哈希值与掩码相与,可以得到哈希值在 0 到 mask 之间的一个数,该数可以用作哈希表中的桶索引。

例如,当 mask 的值为 0b111 时,它的二进制表示为 111,即 3,因此哈希值相与 mask 后只会保留哈希值的低 3 位,得到一个范围在 0 到 3 的整数。这就保证了哈希值可以被映射到哈希表中的某个桶

也就是说,IndexOf()中的取掩码过程中

int mask = (1 << global_depth_) - 1;

因为全局深度global_depth_是与目录大小相关的,global_depth_,就说明在哈希对应索引时需要比对二进制的后几位,因此将1左移global_depth_后减1就得到 global_depth_ 全为1的二进制数,因此mask必然覆盖到整个目录的索引 下标 取值范围

/**
   * @brief For the given key, return the entry index in the directory where the key hashes to.
   * @param key The key to be hashed.
   * @return The entry index in the directory.
   */
//肯定能哈希到目录中对应索引的下标
  auto IndexOf(const K &key) -> size_t {
    int mask = (1 << global_depth_) - 1;
    return std::hash<K>()(key) & mask;
  }

所以Find()Insert()Remove三个函数过程如下,先哈希找到对应的目录索引,然后对该索引shared_ptr<bucket>所指向的桶做对应操作

std::scoped_lock<std::mutex> lock(latch_);
auto index = IndexOf(key);
auto target_bucket = dir_[index];
ops...//return target_bucket->Find(key, value); 直接调用内定义类Bucket的内置插入函数了

同时注意到该函数返回值是size_t类型的,这是因为 在位运算中使用的常量一般都是无符号数,而size_t即为unsigned类型的

Insert()
  1. 根据上面IndexOf()的实现效果,因此外层大循环这里是while,即使内部在上一轮循环中已经对索引目录扩容了,这一轮再次调用IndexOf()一样能实现 从新扩容的目录中哈希到对应索引 的效果,因为每一轮的global_depth_都是动态变化的

    因为全局深度global_depth_是与目录大小相关的,global_depth_ ,就说明在哈希对应索引时需要比对二进制的 后几位 ,因此将1左移global_depth_后减1就得到 global_depth_ 全为1的二进制数,因此mask必然覆盖到整个目录的索引 下标 取值范围

    至于为什么要用while,假如一个桶分裂后且depth_增加后,kv对 再分配过程中 全部分到一个桶,那么这个桶还是满的,就需要再次经历这个过程

  2. global_depth_代表的是kv对在索引目录中 找下标 的哈希二进制位数,而桶的depth_代表的是桶内kv对分布的哈希二进制位数,因此可能多个下标索引指向同一个桶,因为二者不等

  3. 为什么是将目录大小加倍?因为多出来了表示索引的一位,有0/1两种可能,即*2

  4. // double the size of the directory.
            dir_.resize(oldcapacity << 1);
            for (int i = 0; i < oldcapacity; i++) {
              dir_[i + oldcapacity] = dir_[i];
            }
    

    resize()来重新分配数组大小,而新建的所有桶,一开始都是跟前面oldcapacity对应的索引,因为 后global_depth_位 是相同的,此时global_depth_加一代表 在哈希 下标 找索引时要多看前面一位了,而每个桶的深度代表这个桶内的元素后几位是相同的。而oldcapacity又是 2^旧的global_depth_

    这里要重点理解 目录表的元素数量和global_depth_的关系 以及 global_depth_的二进制哈希意义

  5. 这里的 0U 中的 U 表示是一个无符号整数类型,这是为了避免一些编译器对于有符号数进行一些意料之外的转换,比如截断或者符号位扩展等。

    因为在位运算中使用的常量一般都是无符号数,因此将 0 后面加上 U 是一种良好的习惯。

    做位运算需要用unsigned,因此这里i声明为size_t

  6. 这里遍历使用引用&以及const,防止拷贝开销以及引用造成的可能修改,这两个放在一起使用

  7. 在C++中,size()的返回值为size_t,因此这里i声明为size_t,否则后面比较部分会警告

  8. 为什么前面IndexOf()中掩码是 global_depth_位全为1,这里是 depth_位为1?

    因为前者是索引目录中哈希得到下标,需要比对后global_depth_位,

    而这里的逻辑是桶满了,需要对桶里面的元素以及指向桶的索引指针作再分配,是根据depth_新增加的那一位是1还是0来再分配的,而depth_位后面的所有位都是一样的,这样才会一开始被分在一个桶里面

  9. 因为是shared_ptr,因此不用手动释放,将指针分配到两个新桶里面以后,旧的桶就自动被释放了

/**
   *
   * TODO(P1): Add implementation
   *
   * @brief Insert the given key-value pair into the hash table.
   * If a key already exists, the value should be updated.
   * If the bucket is full and can't be inserted, do the following steps before retrying:
   *    1. If the local depth of the bucket is equal to the global depth,
   *        increment the global depth and double the size of the directory.
   *    2. Increment the local depth of the bucket.
   *    3. Split the bucket and redistribute directory pointers & the kv pairs in the bucket.
   *
   * @param key The key to be inserted.
   * @param value The value to be inserted.
   */
  void Insert(const K &key, const V &value) override {
    std::scoped_lock<std::mutex> lock(latch_);

    while (dir_[IndexOf(key)]->IsFull()) {//1
      auto index = IndexOf(key);
      auto target_bucket = dir_[index];

      if (target_bucket->GetDepth() == global_depth_) {
        global_depth_++;
        auto oldcapacity = dir_.size();

        // double the size of the directory.
        dir_.resize(oldcapacity << 1);//3
        for (size_t i = 0; i < oldcapacity; i++) {//4,7
          dir_[i + oldcapacity] = dir_[i];
        }
      }

      // Increment the local depth of the bucket.
      auto bucket_0 = std::make_shared<Bucket>(bucket_size_, target_bucket->GetDepth() + 1);
      auto bucket_1 = std::make_shared<Bucket>(bucket_size_, target_bucket->GetDepth() + 1);

      size_t mask = 1 << target_bucket->GetDepth();//8

      // Split the bucket and redistribute the kv pairs in the bucket.
      for (const auto &item : target_bucket->GetItems()) {//6
        if ((std::hash<K>()(item.first) & mask) == 0U) {//5
          bucket_0.Insert(item.first, item.second);
        } else {
          bucket_1.Insert(item.first, item.second);
        }
      }

      num_buckets_++;

      // cautious size_t,redistribute directory pointers to the splited bucket
      for (size_t i = 0; i < dir_.size(); ++i) {//7
        if (dir_[i] == target_bucket) {
          dir_[i] = ((i & mask) == 0U) ? bucket_0 : bucket_1;
        }
      }
    }//9
    
    //retry inserting
    auto target_bucket = dir_[IndexOf(key)];
    for (auto &item : target_bucket->GetItems()) {
      if (item.first == key) {
        item.second = value;
        return;
      }
    }
    target_bucket.Insert(key, value);
  }
Task #2 - LRU-K Replacement Policy
参考链接
  1. (52条消息) 缓存替换策略:LRU-K算法详解及其C++实现 CMU15-445 Project#1_AntiO2的博客-CSDN博客

  2. 146. LRU 缓存 - 力扣(LeetCode)(关于LRU算法的整体思路见自己写的这道题题解)

  3. (54条消息) 【CMU15-445数据库】bustub Project #1:Buffer Pool_bustub project1_Altair_Alpha_的博客-CSDN博客

完整代码
//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// lru_k_replacer.cpp
//
// Identification: src/buffer/lru_k_replacer.cpp
//
// Copyright (c) 2015-2022, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#include "buffer/lru_k_replacer.h"
#include <cstddef>
#include <exception>
#include <mutex>

namespace bustub {

LRUKReplacer::LRUKReplacer(size_t num_frames, size_t k) : replacer_size_(num_frames), k_(k) {}

auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {
  std::scoped_lock<std::mutex> lock(latch_);
  if (curr_size_ == 0U) {//size_t curr_size_{0};
    return false;
  }
  for (auto i = history_list_.rbegin(); i != history_list_.rend(); i++) {
    auto frame = *i;  // be cautious the erase method will modify the iterator
    if (is_evictable_[frame]) {
      *frame_id = frame;
      curr_size_--;
      access_num_[frame] = 0;
      is_evictable_[frame] = false;
      history_list_.erase(hashmap_[frame]);  // need positive iterator instead of history_list_.erase(i);
      //history_list_.remove(frame);   std::list 中 remove()和erase()是等效的
      hashmap_.erase(frame);
      return true;
    }
  }
  for (auto i = cache_list_.rbegin(); i != cache_list_.rend(); i++) {
    auto frame = *i;
    if (is_evictable_[frame]) {
      *frame_id = frame;
      curr_size_--;
      access_num_[frame] = 0;
      is_evictable_[frame] = false;
      cache_list_.erase(hashmap_[frame]);  // need positive iterator instead of history_list_.erase(i);
      //cache_list_.remove(frame);
      hashmap_.erase(frame);
      return true;
    }
  }
  return false;
}

void LRUKReplacer::RecordAccess(frame_id_t frame_id) {
  std::scoped_lock<std::mutex> lock(latch_);
  if (frame_id > static_cast<int>(replacer_size_)) {  // size_t to int cast
    throw std::exception();
  }
  size_t num = ++access_num_[frame_id];
  if (num == 1) {
    history_list_.push_front(frame_id);
    hashmap_[frame_id] = history_list_.begin();
  } else if (num == k_) {
    history_list_.remove(frame_id);//history_list_.erase(hashmap_[frame_id]);
    cache_list_.push_front(frame_id);
    hashmap_[frame_id] = cache_list_.begin();
  } else if (num > k_) {
    cache_list_.remove(frame_id);//cache_list_.erase(hashmap_[frame_id]);
    cache_list_.push_front(frame_id);
    hashmap_[frame_id] = cache_list_.begin();
  }
  //这里隐含了history_list_的FIFO调度算法,也即目标块访问到,但是access_num_没达到k_次时,什么也不做,就实现了FIFO
}

void LRUKReplacer::SetEvictable(frame_id_t frame_id, bool set_evictable) {
  std::scoped_lock<std::mutex> lock(latch_);
  if (frame_id > static_cast<int>(replacer_size_)) {  // size_t to int cast
    throw std::exception();
  }
  if (hashmap_.find(frame_id) == hashmap_.end()) {//这里的写法与access_num_[frame_id] == 0是等效的,因为access_num_是只增不减的,唯一清零的时候就是页面被Evict()或者Remove()的时候,而这两个时候hashmap_中的映射关系也会被清空
    return;
  }
  if (!is_evictable_[frame_id] && set_evictable) {
    curr_size_++;
  } else if (is_evictable_[frame_id] && !set_evictable) {
    curr_size_--;
  }
  is_evictable_[frame_id] = set_evictable;
}

void LRUKReplacer::Remove(frame_id_t frame_id) {
  std::scoped_lock<std::mutex> lock(latch_);
  if (frame_id > static_cast<int>(replacer_size_)) {  // size_t to int cast
    throw std::exception();
  }
  //这里要先判断访问数量(或者是否在hashmap_中),再判断是否可移除,顺序调转就过不了测试
  size_t num = access_num_[frame_id];
  if (num == 0) {
    return;
  }
  if (!is_evictable_[frame_id]) {
    throw std::exception();
  }
  if (num < k_) {
    history_list_.remove(frame_id);
  } else {
    cache_list_.remove(frame_id);
  }
  curr_size_--;
  hashmap_.erase(frame_id);
  access_num_[frame_id] = 0;
  is_evictable_[frame_id] = false;
}

auto LRUKReplacer::Size() -> size_t {
  std::scoped_lock<std::mutex> lock(latch_);
  return curr_size_;
}

}  // namespace bustub

整体思路与私有成员设计

为了要在 O ( 1 ) O(1) O(1)时间内完成找到LRU块,用时间戳肯定是不行的,一个一个比对时间戳的复杂度为 O ( N ) O(N) O(N)

因此用双向链表(队列)代替时间戳,这样只要按照使用顺序维护队列,就可以 O ( 1 ) O(1) O(1)获得排在队尾的LRU了

同时为了在 O ( 1 ) O(1) O(1)时间内根据key定位任意块(链表无法随机访问),需要用一个哈希表来映射key与块本身(或者value)

这样就完成了 O ( 1 ) O(1) O(1)时间定位任意块,从而任意块的访问可以化为将其移动至队头,以及 O ( 1 ) O(1) O(1)时间从队列中取出LRU块的操作

LRU-K的优化思路见上面链接1,3

即设置两条队列,访问次数未达到k的块存放在普通队列中,达到k的块放在真正的LRU缓存队列中,普通队列使用FIFO淘汰算法,缓存队列用LRU淘汰算法

如果要淘汰块,优先淘汰普通队列的块

同时还要两个哈希表记录每个块访问次数和是否可驱逐

一个哈希表映射块号与块本身,此处注意的点是:用指向该块的 指针或者迭代器 来表示块本身,leetcode那道题也是这么设计的,对于两个缓存队列来说只需要一个映射哈希表即可,原因如下

std::listiterator 不会因其它元素的插入移动删除操作而失效。这个 iterator 记录的位置既可能在 hist_list,也可能在 cache_list中(因为它们都是 std::list<frame_id_t>,所以iterator 类型也是相同的可以通用

private:
  [[maybe_unused]] size_t current_timestamp_{0};//不用时间戳
  
  size_t curr_size_{0};//题目规定了这个是当前Replacer中可以驱逐的块数量
  size_t replacer_size_;//题目规定的块号的上限
  size_t k_;
  std::mutex latch_;

  std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> hashmap_;//值是迭代器,相当于指向块的指针

  std::unordered_map<frame_id_t, bool> is_evictable_;
  std::unordered_map<frame_id_t, int> access_num_;

  //两条缓存队列
  std::list<frame_id_t> history_list_;
  std::list<frame_id_t> cache_list_;
加锁

注意只要是访问私有成员的函数,都要加互斥锁,用到了mutex中的std::scoped_lock

//即使只是读取curr_size_
auto LRUKReplacer::Size() -> size_t {
  std::scoped_lock<std::mutex> lock(latch_);
  return curr_size_;
}
Evict()
  1. 一开始写作这样

    history_list_.erase(i);
    

    报错,因为erase()参数需要正向迭代器,而i为反向迭代器

  2. 如果像下文第一个for循环那样写,会导致段错误

    以测试用例为例子,在vscode的debug:watch栏中设置观察*i

    当前history_list_hashmap_如下,frame_id:1因为被访问了两次,在cache_list_

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    第一轮的Evict()中,*i为2,history_list_.erase(hashmap_[*i])执行完之后,*i变成了3

    因而下一句hashmap_.erase(*i)就将哈希表中页号3的映射关系删除了,而并没有删除页号2

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    到这里还没有报错,但是下一轮Evict()

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    可以看到此时的*i仍然为3,但是hashmap_中已经没有key为3的元素了,但是还存在key为2的元素,此时再步进下去就会报段错误

    因为istd::list history_list_的反向迭代器,在其中的操作是正确的,但是这个结果放到std::unordered_map<> hash_map_身上就变成错误的了

    至于为什么erase()i就自动指向下一个元素了呢?

    记得每次改完代码后跑测试,无论是debug还是跑完整测试,都要新生成测试文件(make extendible_hash_table_test -j$(nproc)),不然一直是旧的结果

    网上找不到对应的资料,都是关于正向迭代器的资料,我测试了一下,把i改为了正向迭代器,并且std::list::erase()之后,*i仍然为5,并没有像反向迭代器似的指向下一个值4外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    但是别以为这个迭代器仍然存在,只是保存了值而已,因此再执行下一句hashmap_.erase(*i)访问迭代器就报错了,因为此时的i已经不存在了

    在执行以后,i所指的对象已经被销毁,所以再对i进行操作是非法的,*i会报错

    ==30140==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000c80 at pc 0x0000004ead4a bp 0x7fffffffab90 sp 0x7fffffffab88
    

    这里参考以下文章

    回到刚才,我猜测因为是反向迭代器与正向迭代器性质不同,因此std::list::erase()i对应的正向迭代器后,反向迭代器i就自动指向下一个元素了(相当于正向迭代器的手动i++),而正向迭代器的操作参见文章

    因此这里访问元素不能依赖迭代器*i,在每次循环开头时就保存迭代器对应页号,用页号操作页面

auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool {
  std::scoped_lock<std::mutex> lock(latch_);
  if (curr_size_ == 0U) {
    return false;
  }
    
  //这是错的
  for (auto i = history_list_.rbegin(); i != history_list_.rend(); i++) {
    // auto frame = *i;  // be cautious the erase method will modify the iterator to the next one
    if (is_evictable_[*i]) {
      *frame_id = *i;
      curr_size_--;
      access_num_[*i] = 0;
      is_evictable_[*i] = false;
      history_list_.erase(hashmap_[*i]);  //1 need positive iterator instead of history_list_.erase(i);
      hashmap_.erase(*i);//2 在这一步之后,下一轮的Evict()调用会报错,这个bug是这两条语句一起作用的效果
      return true;
    }
  }
    
  //这是对的
  for (auto i = cache_list_.rbegin(); i != cache_list_.rend(); i++) {
    auto frame = *i;//用页号访问
    if (is_evictable_[frame]) {
      *frame_id = frame;
      curr_size_--;
      access_num_[frame] = 0;
      is_evictable_[frame] = false;
      cache_list_.erase(hashmap_[frame]);  // need positive iterator instead of history_list_.erase(i);
      hashmap_.erase(frame);
      return true;
    }
  }
  return false;
}
Task #3 - Buffer Pool Manager Instance
参考链接

【CMU15-445数据库】bustub Project #1:Buffer Pool

完整代码
//===----------------------------------------------------------------------===//
//
//                         BusTub
//
// buffer_pool_manager_instance.cpp
//
// Identification: src/buffer/buffer_pool_manager.cpp
//
// Copyright (c) 2015-2021, Carnegie Mellon University Database Group
//
//===----------------------------------------------------------------------===//

#include "buffer/buffer_pool_manager_instance.h"
#include <cstddef>

#include "common/config.h"
#include "common/exception.h"
#include "common/macros.h"

namespace bustub {

BufferPoolManagerInstance::BufferPoolManagerInstance(size_t pool_size, DiskManager *disk_manager, size_t replacer_k,
                                                     LogManager *log_manager)
    : pool_size_(pool_size), disk_manager_(disk_manager), log_manager_(log_manager) {
  // we allocate a consecutive memory space for the buffer pool
  pages_ = new Page[pool_size_];
  page_table_ = new ExtendibleHashTable<page_id_t, frame_id_t>(bucket_size_);
  replacer_ = new LRUKReplacer(pool_size, replacer_k);

  // Initially, every page is in the free list.
  for (size_t i = 0; i < pool_size_; ++i) {
    free_list_.emplace_back(static_cast<int>(i));
  }
}

BufferPoolManagerInstance::~BufferPoolManagerInstance() {
  delete[] pages_;
  delete page_table_;
  delete replacer_;
}

auto BufferPoolManagerInstance::GetAvailableFrame(frame_id_t *outcomeframeid) -> bool {
  frame_id_t frame_id;
  if (!free_list_.empty()) {
    frame_id = free_list_.front();
    free_list_.pop_front();
    *outcomeframeid = frame_id;
    return true;
  }
  if (replacer_->Evict(&frame_id)) {
    if (pages_[frame_id].is_dirty_) {
      disk_manager_->WritePage(pages_[frame_id].page_id_, pages_[frame_id].data_);
      pages_[frame_id].is_dirty_ = false;
    }
    page_table_->Remove(pages_[frame_id].page_id_);
    *outcomeframeid = frame_id;
    return true;
  }
  return false;
}

auto BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) -> Page * {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (GetAvailableFrame(&frame_id)) {
    *page_id = AllocatePage();
    pages_[frame_id].page_id_ = *page_id;
    pages_[frame_id].pin_count_ = 1;
    pages_[frame_id].ResetMemory();
    page_table_->Insert(*page_id, frame_id);
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
    return &pages_[frame_id];
  }
  page_id = nullptr;
  return nullptr;
}

auto BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) -> Page * {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (page_table_->Find(page_id, frame_id)) {
    pages_[frame_id].pin_count_++;
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
    return &pages_[frame_id];
  }
  if (GetAvailableFrame(&frame_id)) {
    page_table_->Insert(page_id, frame_id);
    pages_[frame_id].page_id_ = page_id;
    pages_[frame_id].pin_count_ = 1;
    pages_[frame_id].ResetMemory();
    disk_manager_->ReadPage(page_id, pages_[frame_id].GetData());
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
    return &pages_[frame_id];
  }
  return nullptr;
}

auto BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (!page_table_->Find(page_id, frame_id) || pages_[frame_id].pin_count_ == 0) {
    return false;
  }
  if (--pages_[frame_id].pin_count_ == 0) {
    replacer_->SetEvictable(frame_id, true);
  }
  if (!pages_[frame_id].is_dirty_) {
    pages_[frame_id].is_dirty_ = is_dirty;
  }
  return true;
}

auto BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) -> bool {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (!page_table_->Find(page_id, frame_id)) {
    return false;
  }
  disk_manager_->WritePage(page_id, pages_[frame_id].GetData());
  pages_[frame_id].is_dirty_ = false;
  return true;
}

void BufferPoolManagerInstance::FlushAllPgsImp() {
  std::scoped_lock lock(latch_);
  for (size_t i = 0; i < pool_size_; i++) {
    if (pages_[i].page_id_ != INVALID_PAGE_ID) {
      disk_manager_->WritePage(pages_[i].page_id_, pages_[i].GetData());
      pages_[i].is_dirty_ = false;
    }
  }
}

auto BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) -> bool {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (!page_table_->Find(page_id, frame_id)) {
    return true;
  }
  if (pages_[frame_id].pin_count_ != 0) {
    return false;
  }
  if (pages_[frame_id].is_dirty_) {
    disk_manager_->WritePage(pages_[frame_id].page_id_, pages_[frame_id].data_);
    pages_[frame_id].is_dirty_ = false;
  }
  pages_[frame_id].ResetMemory();
  pages_[frame_id].pin_count_ = 0;
  pages_[frame_id].page_id_ = INVALID_PAGE_ID;
  page_table_->Remove(page_id);
  free_list_.emplace_back(frame_id);
  replacer_->Remove(frame_id);
  DeallocatePage(page_id);
  return true;
}

auto BufferPoolManagerInstance::AllocatePage() -> page_id_t { return next_page_id_++; }

}  // namespace bustub

设计思路

bustub 中有 Page 和 Frame 的概念,Page 是承载 4K 大小数据的类,可以通过 DiskManager 从磁盘文件中读写,带有 page_id 编号,is_dirty 标识等信息。Frame 不是一个具体的类,而可以理解为 Buffer Pool Manager中容纳 Page 的槽位,具体来说,

  • BPM 中有一个 Page 数组,这个page数组作为实际的buffer pool内存池
  • frame_id 就是某个 Page 在该数组中的下标,也即使用哈希表映射page页号与其在数组中下标frame_id的关系,而此哈希表即为页表page table
    • 此处的页表哈希方法就用我们之前实现的ExtendibleHashTable方法,其实看成stl中的unordered_map简单映射功能就行

参考下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外界只知道 page_id,向 BPM 查询时,BPM 要确定该 Page 是否存在以及其位置,所以要维护一个 page_id 到 frame_id 的映射,其实现就使用我们刚完成的 ExtendibleHashTable。为区分空闲和占用的 Page,维护一个 free_list,保存空闲的 frame_id。初始状态,所有 Page 都是空闲的。当上层需要取一个 Page 时,如果 Page 已存在于 BP 中,则直接返回;否则需要从磁盘读取到 BP 中。此时优先取空闲的 Page,否则只能从所有已经占用的 Page 中用我们刚完成的 LRUKReplacer 决定踢出某个 Page。

因此总结如下:外界知道page_id,要访问某页面时,buffer pool内部在 哈希表/页表 通过该page_id查询到对应的frame_id,用该frame_id作为下标从实际page数组中取得对应的Page对象,Page对象中包含了实际的存储数据data_,页号page_id_pin_count,脏位id_dirty_

  • 开局的pool_size限制的是Frame的数量,影响到Page数组大小,以及LRU驱逐策略的块数量

实际上bpm统筹的是三个半模块(具体可见下文 分配新页

  1. 页表/哈希表
  2. 页面元数据与标志位
  3. 驱逐策略
  4. 其中2中的标志位pin_count_与3中的Evictable状态是对应的(见下文)
  5. 2中的is_dirty标志位比较特殊,除非手动,只有bpm将页面写回磁盘的时候会重置,其他时间不会有行为改变该标志位
  6. 剩下的半个模块就是获取Frame以及释放Frame的时候维护的链表std::list<frame_id_t>
pin_count_的两个功能

表示当前有几个进程在使用/需要这个页面,每个需求都对应着一个pin_count_,当进程用完了不需要这个页面了,就unpin

如果一个页面pin_count_为0,即没有进程需要这个页面了,就可以被驱逐,只不过是懒驱逐,没有空闲Frame的时候再去释放,这个页面实际上在不在buffer pool中已经无所谓了

既控制LRU驱逐策略中页是否evictable,又控制DeletePgImp方法是否可直接删除该页

本质上是同一件事情,只要不主动调用UnpinPgImp方法减少页的pin_count_,那么分配新页的两个方法FetchPgImpNewPgImp都只会将pin_count_置为1或者增加,那么无论是主动删除还是驱逐策略,都无法去除该页

在代码中,也即pin_count_的增减,是与replacer_->SetEvictable方法绑定的

类型别名
//src/include/common/config.h
using frame_id_t = int32_t;    // frame id type
using page_id_t = int32_t;     // page id type
GetAvailableFrame

因为NewPgImpFetchPgImp都需要得到一个空闲的Frame来安置新的page,而空闲的Frame要么从free_list_ 中获得,要么根据LRU模块的Evict()结果决定驱逐出当前的哪一个Frame,注意这里 replacer_模块只负责驱逐策略部分,不负责实际执行

因此设计一个返回值为bool类型的函数,这样NewPgImpFetchPgImp调用时就能直接知道能不能分配到Frame,而实际的Frame号通过参数列表传出,其实就和replacer_->Evict(&frame_id)用法一样

驱逐得到空Frame的本质就是从 页表/哈希表 中移除映射关系,实际的数据以及页号更替工作滞后到写入新页信息的时候

auto BufferPoolManagerInstance::GetAvailableFrame(frame_id_t *outcomeframeid) -> bool {
  //从free_list_获得空闲的Frame
  frame_id_t frame_id;
  if (!free_list_.empty()) {
    frame_id = free_list_.front();
    free_list_.pop_front();
    *outcomeframeid = frame_id;
    return true;
  }
   
  //否则看是否能驱逐某一块,若能驱逐,传入的frame_id中就保存了块号,那么这个if体内就是做实际驱逐的事情
  if (replacer_->Evict(&frame_id)) {
    if (pages_[frame_id].is_dirty_) {//如果是脏页,在实际驱逐之前就要写回磁盘
      disk_manager_->WritePage(pages_[frame_id].page_id_, pages_[frame_id].data_);
      pages_[frame_id].is_dirty_ = false;//这里写入脏页后要重置脏位,因为上层函数不会做这个事情,只会重写页号等标志位和实际数据部分
    }
    page_table_->Remove(pages_[frame_id].page_id_);//驱逐的本质就是从 页表/哈希表 中移除映射关系,其实页中的数据还是原样,但只要页表里面没有页号frame号键值对了,那么这一页就没了
    *outcomeframeid = frame_id;
    return true;
  }
  return false;
}
分配新页
  1. 通过上文的GetAvailableFrame分配一个空闲Frame,如果没有直接退出
  2. 获得页号,其中
    • NewPgImp是根据AllocatePage()自动分配的,也就是每分配一个页号,全局变量next_page_id_自增
    • FetchPgImp中页号是外面提供的
  3. 页表/哈希表中记录页号到Frame号的映射关系,这一步相当于「在buffer pool层面写入了新页」(因为buffer pool与外界的唯一接口就是页表)
    • 上面的AllocatePage()过程中,驱逐某一页的实际操作也是在从 页表/哈希表 中移除映射关系,而并没有清空元数据**(懒政)**,因为没必要
  4. 但是获得新页与驱逐得到Frame不同,buffer pool内部还需要做「实际数据以及标志」层面上的更改
    • 页中写入新的页号,pin_count_ ,如果该页已经在buffer中,pin_count_++,否则是第一次替换到buffer中,置为1
    • 元数据处理
      • 对于NewPgImp,因为是获取一个新的空页面,因此pages_[frame_id].ResetMemory()将元数据清空即可
      • 对于FetchPgImp,如果老页面在buffer中,啥也不用做,如果不在,则需要清空原数据后,调用磁盘接口将新数据读取到data_
  5. 最后一步,需要更新驱逐策略,包括更新访问次数,并且被访问到的页设置为pinned(后面会有专门的unpin函数操作)
auto BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) -> Page * {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (GetAvailableFrame(&frame_id)) {
      
    *page_id = AllocatePage();
      
    page_table_->Insert(*page_id, frame_id);//在buffer pool层面写入了新页
    
    //**做「实际数据以及标志」层面上的更改**
    pages_[frame_id].page_id_ = *page_id;
    pages_[frame_id].pin_count_ = 1;
    pages_[frame_id].ResetMemory();
     
    //更新驱逐策略  
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
      
    return &pages_[frame_id];
  }
  page_id = nullptr;
  return nullptr;
}

auto BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) -> Page * {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (page_table_->Find(page_id, frame_id)) {
    
    //**做「实际数据以及标志」层面上的更改**
    pages_[frame_id].pin_count_++;
      
    //更新驱逐策略
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
      
    return &pages_[frame_id];
  }
  if (GetAvailableFrame(&frame_id)) {
      
    page_table_->Insert(page_id, frame_id);//在buffer pool层面写入了新页
      
    //**做「实际数据以及标志」层面上的更改**
    pages_[frame_id].page_id_ = page_id;
    pages_[frame_id].pin_count_ = 1;
    pages_[frame_id].ResetMemory();
    disk_manager_->ReadPage(page_id, pages_[frame_id].GetData());
      
    //更新驱逐策略
    replacer_->RecordAccess(frame_id);
    replacer_->SetEvictable(frame_id, false);
      
    return &pages_[frame_id];
  }
  return nullptr;
}
UnpinPgImp

如果只访问页面的话,pin_count_是只增不减的,因此设置了手动unpin的方法,pin_count_为0时候,该页在驱逐策略中自动变为Evictable状态

auto BufferPoolManagerInstance::UnpinPgImp(page_id_t page_id, bool is_dirty) -> bool {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (!page_table_->Find(page_id, frame_id) || pages_[frame_id].pin_count_ == 0) {
    return false;
  }
  if (--pages_[frame_id].pin_count_ == 0) {
    replacer_->SetEvictable(frame_id, true);
  }
  if (!pages_[frame_id].is_dirty_) {//顺带根据参数设置了脏页状态,只能由干净页面变为脏页,反之不可行
    pages_[frame_id].is_dirty_ = is_dirty;
  }
  return true;
}
FlushAllPgsImp()

因为我们自己实现的ExtendibleHashTable只提供了Find,Insert,Remove三个接口,因此无法用作遍历对象

因此直接在buffer数组中遍历所有Frame,如果有页号,就说明该页是solid的,可以操作

注意page_是指针数组,不能用基于范围的循环:

因此用size_t的 Frame号遍历数组

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

void BufferPoolManagerInstance::FlushAllPgsImp() {
  std::scoped_lock lock(latch_);
  // pages_ is a pointer-form array, can't use range-for
  for(size_t i = 0;i<pool_size_;i++){
    if(pages_[i].page_id_!=INVALID_PAGE_ID){
      disk_manager_->WritePage(pages_[i].page_id_, pages_[i].GetData());
      pages_[i].is_dirty_ = false;
    }
  }
}

删除页面

auto BufferPoolManagerInstance::DeletePgImp(page_id_t page_id) -> bool {
  std::scoped_lock lock(latch_);
  frame_id_t frame_id;
  if (!page_table_->Find(page_id, frame_id)) {
    return true;
  }
  //pin_count和Evictable是对应的
  if (pages_[frame_id].pin_count_ != 0) {
    return false;
  }
  if (pages_[frame_id].is_dirty_) {
    disk_manager_->WritePage(pages_[frame_id].page_id_, pages_[frame_id].data_);
    pages_[frame_id].is_dirty_ = false;
  }
    
  //清除元数据,标志位,记得一定要将page_id设置为无效
  pages_[frame_id].ResetMemory();
  pages_[frame_id].pin_count_ = 0;
  pages_[frame_id].page_id_ = INVALID_PAGE_ID;
  
  //页表,驱逐策略中去除该Frame,空闲链表中加入该Frame
  page_table_->Remove(page_id);
  free_list_.emplace_back(frame_id);
  replacer_->Remove(frame_id);
  
  //释放页号
  DeallocatePage(page_id);
  return true;
}

Project #2 - B+Tree

参考链接
  1. B树和B+树的插入、删除图文详解 - nullzx - 博客园 (cnblogs.com)
  2. 【CMU15-445数据库】bustub Project #2:B+ Tree(上)-CSDN博客
  3. 从B树、B+树、B*树谈到R 树-CSDN博客

CHECKPOINT #1

设计思路

观察src/include/storage/index/b_plus_tree.h私有成员

键值的存储,第一个键不算

从相应的testtest/storage/b_plus_tree_insert_test.cpp文件中可以看出模板是如何实例化的(学会看头文件),此处使用他们自己在src/include/storage/index/generic_key.h定义的GenericKeyGenericComparator类来实例化泛型,而各类参数如INTERNAL_PAGE_SIZE以及别名等都是在src/include/common/config.h文件中定义的

//test/storage/b_plus_tree_insert_test.cpp  GenericKey<8>,GenericComparator<8>进行实例化部分
BPlusTree<GenericKey<8>, RID, GenericComparator<8>> tree("foo_pk", bpm, comparator, 2, 3);
//src/include/storage/index/b_plus_tree.h
#define BPLUSTREE_TYPE BPlusTree<KeyType, ValueType, KeyComparator>
//src/include/storage/page/b_plus_tree_page.h
#define INDEX_TEMPLATE_ARGUMENTS template <typename KeyType, typename ValueType, typename KeyComparator>
类型转换
  1. C++基础#22:C++中的静态强制static_cast_c++ static_cast-CSDN博客
  2. C++基础#23:C++中的reinterpret_cast,数据类型转换-CSDN博客

reinterpret_cast运算符允许将任意指针转换到其他指针类型,也允许做任意整数类型和任意指针类型之间的转换。转换时,执行的是逐个比特复制的操作。reinterpret中文意为“重新解释; 重新诠释;”。

//src/include/storage/page/page.h 

/** The actual data that is stored within a page. */
  char data_[BUSTUB_PAGE_SIZE]{};//这里{}是静态数组的初始化

/** @return the actual data contained within this page */
  inline auto GetData() -> char * { return data_; }

而我们的代码中,这里直接将Page对象中 字符数组形式表示的 页内存空间 data_ 重新转义 为 树页面类 BPlusTreePage*,也即「同一块内存空间的不同释义」

auto tree_page = reinterpret_cast<BPlusTreePage* >(page->GetData());

static_cast可以用于类层次结构中 基类和子类之间指针或引用 的转换

这里InternalPageBPlusTreePage的子类,因此父类指针转换为子类指针

auto internal_page = static_cast<InternalPage*>(tree_page);

reinterpret_cast和static_cast的比较:

static_cast就是利用C++类型之间的继承关系图和聚合关系图(编译器必须知道),根据一个子对象地址计算另一个子对象的地址。

reinterpret_cast不关心继承关系,直接把数据类型A的地址「解释」成另一个数据类型B的地址。

所以,对于无继承关系的类的转换,static_cast需要进行构造函数的重载,参数必须是要被转换的类的类型。而reinterpret_cast则没有这个限制。

回到这个项目里面的逻辑

内部节点和叶节点对象都不是直接创建出来而是由一个 Buffer Pool 管理的 「Page 的 data 部分」类型转化而来(所以要用到很少用很暴力的 reinterpret_cast)。因此,节点对象的生命周期也不是由 new 和 delete,而是由我们上节实现的 BufferPoolManager 管理:取一个页面,用 FetchPage;使用结束归还一个页面,用 UnpinPage

Each B+Tree leaf/internal page corresponds to the content (i.e., the data_ part) of a memory page fetched by buffer pool. So every time you try to read or write a leaf/internal page, you need to first fetch the page from buffer pool using its unique page_id, then reinterpret cast to either a leaf or an internal page, and unpin the page after any writing or reading operations.

查找(GetValue)

给定一个键 x,查找其是否在 B+ 树中存在。实现逻辑是先找到键可能在的叶节点,然后扫描一遍叶节点的内容确定是否存在,其中重点是前者。

编写一个函数 GetLeafPage,根据 B+ 树的规则,应该从根节点开始,每次在内部节点中找到 k i < x < k i + 1 k_i < x < k_i+1 ki<x<ki+1 的位置,然后沿着 v i v_i vi指针继续向下,直到达到叶节点。

逻辑是

  1. 一个while循环,先从buffer_pool中取出一个普通的Page,然后把Page中的data_部分转义(reinterpret)为BPlusTreePage *
  2. 然后这个被转义的BPlusTreePage *对象就可以正常调用BPlusTreePage *类的函数判断是否为叶子页了
  3. 如果是叶子页,找到了,返回该页最初的Page形态,而非加工过后的BPlusTreePage *形态
  4. 不是,那么就遍历这个页面所有的键值对,找到 k i < x < k i + 1 k_i < x < k_i+1 ki<x<ki+1 的位置,然后把这个指针指向的页作为下一页
    • 这里的遍历解释见下文插入Insert部分
  5. 手动控制页对象的生命周期,因为有了FetchPage()新建页对象,那么需要UnPin()来释放页对象
  6. 下一轮循环

其中

Tips:循环时找内部节点中第一个比 x 大的键取其左侧的值即可(想象一个数组,每个数组下标对应的值是其指针,而k[0]无效,这使得第一个key有了左边的指针,而k[1]变成了其右指针,同时是第二个key的左指针,这样就实现了key比value少一个,同时每个key都有左右两个value),而这样 不能 探测到 x 比所有 k 都大的情况,所以要将 next_page_id 初始化为最右侧的键

//前两行的宏定义见上文
INDEX_TEMPLATE_ARGUMENTS
auto BPLUSTREE_TYPE::GetLeafPage(const KeyType &key) -> Page * {
  page_id_t next_page_id = root_page_id_;
  while (true) {
    //先通过buffer_pool取出一个page//as long as there is a FetchPage(), there must be a corresponding Unpin() afterwards
    Page *page = buffer_pool_manager_->FetchPage(next_page_id);
    
    //根据上面所说的,把Page中的data_部分转义(reinterpret)为BPlusTreePage *
    auto tree_page = reinterpret_cast<BPlusTreePage *>(page->GetData());
    //然后就可以正常调用BPlusTreePage *类的函数判断是否为叶子页了
    if (tree_page->IsLeafPage()) {
      return page;
    }
    //如果不是,static_cast用于父子类指针之间的转换
    auto internal_page = static_cast<InternalPage *>(tree_page);
    //将 `next_page_id` 初始化为最右侧的键
    next_page_id = internal_page->ValueAt(internal_page->GetSize() - 1);
    //从int i =1开始,省略第一个key的空间,也就是KeyAt(0) is neglected
    for (int i = 1; i < internal_page->GetSize(); i++) {
      if (comparator_(internal_page->KeyAt(i), key) > 0) {//这里使用GenericComparator来实例化模板中的KeyComparator,声明为comparator_,因此观察src/include/storage/index/generic_key.h中的模板类,直接用重载的()来判断大小
        next_page_id = internal_page->ValueAt(i - 1);
        break;
      }
    }
    //the corresponding UnpinPage() to release this Page, inside this while() loop
    buffer_pool_manager_->UnpinPage(page->GetPageId(), false);
  }
}
//src/storage/index/b_plus_tree.cpp 为上面代码的实例化补充,这里可见comparator_是被const KeyComparator &comparator初始化的,而KeyComparator被上面的GenericComparator<8>进行实例化
INDEX_TEMPLATE_ARGUMENTS
BPLUSTREE_TYPE::BPlusTree(std::string name, BufferPoolManager *buffer_pool_manager, const KeyComparator &comparator,
                          int leaf_max_size, int internal_max_size)
    : index_name_(std::move(name)),
      root_page_id_(INVALID_PAGE_ID),
      buffer_pool_manager_(buffer_pool_manager),
      comparator_(comparator),
      leaf_max_size_(leaf_max_size),
      internal_max_size_(internal_max_size) {}

在找到对应叶子页的基础上,GetValue()

INDEX_TEMPLATE_ARGUMENTS
auto BPLUSTREE_TYPE::GetValue(const KeyType &key, std::vector<ValueType> *result, Transaction *transaction) -> bool {
  bool ret = false;
  Page *page = GetLeafPage(key);
  auto tree_page = reinterpret_cast<LeafPage *>(page->GetData());
  //注意这里for循环遍历下标,对数组每个元素的key进行比对找到键值对
  for (int i = 0; i < tree_page->GetSize(); i++) {
    if (comparator_(tree_page->KeyAt(i), key) == 0) {
      result->emplace_back(tree_page->ValueAt(i));
      ret = true;
      break;
    }
  }
  //这里要Unpin()掉上面GetLeafPage()中Fetch但是没有走完while循环旧返回的page
  buffer_pool_manager_->UnpinPage(page->GetPageId(), false);
  return ret;
}
插入(Insert)

B+ 树的插入流程为:

  1. 如果是空树,创建一个叶节点作为根。注意涉及 root_page_id_ 更新时都要调用一次已经提供的函数 UpdateRootPageId(),如果是第一次创建传 1 作为参数,更新不用传任何参数,具体看b_plus_tree.cpp里面的UpdateRootPageId()函数

    • 这里对叶节点初始化

      leaf_page->SetValueAt(0, value);
      leaf_page->SetKeyAt(0, key);
      

      见下文的键值对实际存储方式为std::pair<KeyType, ValueType>的数组,因此新建**叶子页面(因此下标为0处也是有效的,如果是内部页面,下标从1开始标识key,下标index为0处没有key,只有value)**的第一个键值对的下标index设置为0

    • //注意插入键值对的时候控制size
      leaf_page->IncreaseSize(1);
      

      用于控制当前页面键值对pair数组的size,与下面的for循环遍历全部键值对中的结束条件对应

      //使用「当前页面键值对pair数组的size」作为结束控制条件,遍历整个数组,得到key和value 
      for (int i = 0; i < leaf_page->GetSize(); i++)
      
  2. 如果不是空树,从根节点向下查找到键值应该所在的叶节点。文档说明了**「不支持重复键」**,所以先扫描一遍叶节点,如果发现键存在则直接返回 false

    • 因为这种用键值对pair的数组的存储方式,上面的查找以及下面中的for循环遍历全部键值对找到是否有重复键/找到下一个页面的键,都是从0到当前页面的size遍历所有的下标index,使用ValueAt()KeyAt()返回对应的键值,根据b_plus_tree_leaf_page.cpp中发现这两个函数就是返回pair.first/pair.second

    • 无法像stl里面的map一样,直接获得key,必须遍历找到key

      INDEX_TEMPLATE_ARGUMENTS
      auto BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) -> bool {
        //如果是空树
        if (IsEmpty()) {
          //首先从buffer_pool获取一个新的页面,以及对应的页面id,注意这里与FetchPage的区别,后者是指定id获取页面,前者是单纯的返回一个页面
          Page *page = buffer_pool_manager_->NewPage(&root_page_id_);
          UpdateRootPageId(1);//依葫芦画瓢
          auto leaf_page = reinterpret_cast<LeafPage *>(page->GetData());
          //对这个叶节点做初始化操作,注意SetValueAt()和SetKeyAt()都是要「自己主动」在b_plus_tree_leaf_page.h中加上函数签名的
          //同时需要在b_plus_tree_page.h中把private数据成员的「不使用」标志去除
          leaf_page->Init(root_page_id_, INVALID_PAGE_ID, leaf_max_size_);
          leaf_page->SetValueAt(0, value);
          leaf_page->SetKeyAt(0, key);
          //这里注意一定要IncreaseSize(1)
          leaf_page->IncreaseSize(1);
          leaf_page->SetNextPageId(INVALID_PAGE_ID);
          //记得bufferpool中,有获取页面就一定有Unpin,同时页面被修改了,dirtybit置为脏
          buffer_pool_manager_->UnpinPage(root_page_id_, true);
          return true;
        }
          
        //因为这里Fetchpage了,需要在接下来的所有分支里面Unpin
        Page *page = GetLeafPage(key);
          
        auto leaf_page = reinterpret_cast<LeafPage *>(page->GetData());
        //遍历键值对pair数组,看是否有相同的key,因为不支持重复键
        for (int i = 0; i < leaf_page->GetSize(); i++) {
          if (comparator_(leaf_page->KeyAt(i), key) == 0) {
            buffer_pool_manager_->UnpinPage(leaf_page->GetPageId(), false);//需要在接下来的所有分支里面Unpin
            return false;
          }
        }
        return false;
      }
      

      array_[1] 等价于一个指针,按照一般习惯应该在叶子页面的构造函数中为其 new 出一片大小为 max_size_ 的空间,作为实际页面的存放空间,但实际上不需要这样做,因为:

      internal/leaf节点对象使用的是buffer_pool中分配好的固定空间,他们的私有成员array_ 可以控制从该位置开始到 Page 的 data 结束为止的这一段空间

      注意数据实际存储为std::pair<KeyType, ValueType>的数组,使用array_[1] 等价的指针来访问这个数组使用index作为数组下标,访问pair对中的first second元素,对应的是key和value

      //b_plus_tree_leaf_page.h/b_plus_tree_internal_page.h
      #define MappingType std::pair<KeyType, ValueType>
      ...
      class BPlusTreeInternalPage : public BPlusTreePage {
      ...
      private:
        // Flexible array member for page data.
        MappingType array_[1];
      }
      
      //「自己主动」在b_plus_tree_leaf_page.h中加上函数签名
      void SetKeyAt(int index, const KeyType &key);
      void SetValueAt(int index, const ValueType &value);
      
      //b_plus_tree_leaf_page.cpp,注意这里的方式,使用index作为数组下标,访问pair对中的first second元素,对应的是key和value
      INDEX_TEMPLATE_ARGUMENTS
      void B_PLUS_TREE_LEAF_PAGE_TYPE::SetKeyAt(int index, const KeyType &key) { array_[index].first = key; }
      INDEX_TEMPLATE_ARGUMENTS
      void B_PLUS_TREE_LEAF_PAGE_TYPE::SetValueAt(int index, const ValueType &Value) { array_[index].second = Value; }
      
      INDEX_TEMPLATE_ARGUMENTS
      auto B_PLUS_TREE_LEAF_PAGE_TYPE::KeyAt(int index) const -> KeyType { return array_[index].first; }
      
  3. 未溢出情况插入的具体逻辑可以放到 LeafPage 类中做,所以添加一个 Insert 函数,找到插入位置,将所有后面的键值对后移一位,再设置。由于 array_ 是有序的,如果还想提高效率,可以把找插入位置用二分搜索实现。

    • 注意插入键值对的时候控制页面size

    Tips:comparator_ 也要作为参数传入 Insert,否则 LeafPage 中无法进行键的比较,也就无法查找

    //b_plus_tree.cpp 书接上文
    INDEX_TEMPLATE_ARGUMENTS
    auto BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) -> bool {
      ...
      ...
    leaf_page->Insert(key, value, comparator_);//comparator_也要传入
      if (leaf_page->GetSize() < leaf_max_size_) {//判断为没有溢出,注意这里的size是「严格小于」max_size的
        buffer_pool_manager_->UnpinPage(leaf_page->GetPageId(), false);
        return true;
      }
    
    //b_plus_tree_leaf_page.h
    INDEX_TEMPLATE_ARGUMENTS
    void B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, KeyComparator comparator_) {
      int size = GetSize();
      if (size >= GetMaxSize()) {
        throw std::logic_error("B+ tree leaf is full,please split this page beforehand");
      }
      int insert_pos = 0;
      //先使用传入的comparator_遍历pair对数组比较key大小,找到第一个大于给定key的下标index,也就是insert_position
      while (insert_pos < size && comparator_(KeyAt(insert_pos), key) < 0) {
        insert_pos++;
      }
      //从尾部开始倒序将所有后面的键值对后移一位
      for (int i = size; i > insert_pos; i--) {
        array_[i] = array_[i - 1];
      }
      //「注意插入键值对的时候控制页面size」
      IncreaseSize(1);
      SetKeyAt(insert_pos, key);
      SetValueAt(insert_pos, value);
    }
    

}
return false;
}
```

 `array_[1]` 等价于一个指针,按照一般习惯应该在叶子页面的构造函数中为其 new 出一片大小为 `max_size_` 的空间,作为实际页面的存放空间,但实际上不需要这样做,因为:

 **internal/leaf节点对象使用的是buffer_pool中分配好的固定空间,他们的私有成员`array_` 可以控制从该位置开始到 Page 的 data 结束为止的这一段空间**

 **注意数据实际存储为`std::pair<KeyType, ValueType>`的数组,使用`array_[1]` 等价的指针来访问这个数组**,**使用index作为数组下标**,访问pair对中的first second元素,对应的是key和value

 ```cpp
 //b_plus_tree_leaf_page.h/b_plus_tree_internal_page.h
 #define MappingType std::pair<KeyType, ValueType>
 ...
 class BPlusTreeInternalPage : public BPlusTreePage {
 ...
 private:
   // Flexible array member for page data.
   MappingType array_[1];
 }
 
 //「自己主动」在b_plus_tree_leaf_page.h中加上函数签名
 void SetKeyAt(int index, const KeyType &key);
 void SetValueAt(int index, const ValueType &value);
 
 //b_plus_tree_leaf_page.cpp,注意这里的方式,使用index作为数组下标,访问pair对中的first second元素,对应的是key和value
 INDEX_TEMPLATE_ARGUMENTS
 void B_PLUS_TREE_LEAF_PAGE_TYPE::SetKeyAt(int index, const KeyType &key) { array_[index].first = key; }
 INDEX_TEMPLATE_ARGUMENTS
 void B_PLUS_TREE_LEAF_PAGE_TYPE::SetValueAt(int index, const ValueType &Value) { array_[index].second = Value; }
 
 INDEX_TEMPLATE_ARGUMENTS
 auto B_PLUS_TREE_LEAF_PAGE_TYPE::KeyAt(int index) const -> KeyType { return array_[index].first; }
 ```
  1. 未溢出情况插入的具体逻辑可以放到 LeafPage 类中做,所以添加一个 Insert 函数,找到插入位置,将所有后面的键值对后移一位,再设置。由于 array_ 是有序的,如果还想提高效率,可以把找插入位置用二分搜索实现。

    • 注意插入键值对的时候控制页面size

    Tips:comparator_ 也要作为参数传入 Insert,否则 LeafPage 中无法进行键的比较,也就无法查找

    //b_plus_tree.cpp 书接上文
    INDEX_TEMPLATE_ARGUMENTS
    auto BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) -> bool {
      ...
      ...
    leaf_page->Insert(key, value, comparator_);//comparator_也要传入
      if (leaf_page->GetSize() < leaf_max_size_) {//判断为没有溢出,注意这里的size是「严格小于」max_size的
        buffer_pool_manager_->UnpinPage(leaf_page->GetPageId(), false);
        return true;
      }
    
    //b_plus_tree_leaf_page.h
    INDEX_TEMPLATE_ARGUMENTS
    void B_PLUS_TREE_LEAF_PAGE_TYPE::Insert(const KeyType &key, const ValueType &value, KeyComparator comparator_) {
      int size = GetSize();
      if (size >= GetMaxSize()) {
        throw std::logic_error("B+ tree leaf is full,please split this page beforehand");
      }
      int insert_pos = 0;
      //先使用传入的comparator_遍历pair对数组比较key大小,找到第一个大于给定key的下标index,也就是insert_position
      while (insert_pos < size && comparator_(KeyAt(insert_pos), key) < 0) {
        insert_pos++;
      }
      //从尾部开始倒序将所有后面的键值对后移一位
      for (int i = size; i > insert_pos; i--) {
        array_[i] = array_[i - 1];
      }
      //「注意插入键值对的时候控制页面size」
      IncreaseSize(1);
      SetKeyAt(insert_pos, key);
      SetValueAt(insert_pos, value);
    }
    
  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值