class TrieNode {
public:
// Create a TrieNode with no children.
TrieNode() = default;//声明默认构造器,等效于TrieNod(){},C++11特性
...
}
explicit TrieNode(std::map<char, std::shared_ptr<const TrieNode>> children) : children_(std::move(children)) {}
explicit
关键字用于阻止C++进行隐式类型转换,强制要求传入参数的类型必须符合声明。
函数(类型 参数名):属性成员名(参数名){}
等价于函数(类型 参数名){this.属性成员名=参数名;}
std::move()
将对象的状态或所有权转换给另一对象,这里是调用构造函数时传入的对象children
到children_
,之后传入的对象就为空了,这样做只是转移,没有内存的搬迁或者内存拷贝,如果不加则需要先将传入对象拷贝一份为临时对象,然后给属性成员赋值,之后销毁临时对象。因此,通过std::move()
,可以避免不必要的拷贝操作。这样的构造函数称为移动构造函数。
std::shared_ptr
智能指针之一,共享指针类,实例化后使用同普通指针,有一些成员函数,有引用计数能力,有一个智能指针指向一块内存,同样指向该内存的关联指针的引用计数加一,反之减一,当计数为0时最后一个智能指针使用delete函数删除内存。
virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }
函数() const {...}
作用是标识成员函数隐式传入的参数this为常量,这样该成员函数就不允许修改当前类的属性了
auto 函数() -> 类型{...}
作用是推导出函数的返回类型
std::unique_ptr
智能指针之一,唯一指针类,拥有管理内存的所有权,没有拷贝构造函数,只有移动构造函数,不能多个unique_ptr对象共享一段内存
std::make_unique<>()
尽可能避免将裸指针传递给一个std::unique_ptr
的构造函数,常用的替代手法是使用std::make_unique<>()
,std::make_shared
同理
auto Trie::Get(std::string_view key) const -> const T * {
C++中与字符串有两种风格,分别是C风格的字符串、std::string
字符串。C风格的字符串性能更高,但是也不方便操作使用。C++17中我们可以使用std::string_view
来获取一个字符串的视图,字符串视图并不真正的创建或者拷贝字符串,只是记录了自己对应的字符串的指针和偏移位置,拥有一个字符串的查看功能,性能比std::string
高很多
std::map<char, std::shared_ptr<const TrieNode>> children_;
该字典树中结点TrieNode本质是一个map,key是键中的下一个字符,value是对应的子节点,对于叶子节点,有TrieNodeWithValue类继承TrieNode,多了个std::shared_ptr
return std::make_shared<TrieNodeWithValue<T>>(std::make_shared<T>(std::forward<T>(value)));
std::forward
通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
引用折叠:形如int &&a
和int &a
的参数
下文取自https://www.zhihu.com/question/40346748/answer/2971112947
void func(int &&a/*右值引用*/, int &b/*左值引用*/){}
//左值引用要传入左值,右值引用同理
template<typename T>
void fun(T &&t/*万能引用*/){}//可以传左值或右值
//但是万能引用如果传入左值引用则报错
int a = 1;
int &b = a;//b为左值引用
编译器允许在一定的情况下进行隐含的多层引用推导,这就是 reference collapsing (引用折叠)。C++中有两种引用(左值引用和右值引用),因此引用折叠就有四种组合。引用折叠的规则:
如果两个引用中至少其中一个引用是左值引用,那么折叠结果就是左值引用;否则折叠结果就是右值引用。
using T = int &;
T& r1; // int& & r1 -> int& r1
T&& r2; // int& && r2 -> int& r2
using U = int &&;
U& r3; // int&& & r3 -> int& r3
U&& r4; // int&& && r4 -> int&& r4
//可以把单个&当做1,&&当做0,做一个OR运算
using T = int &;
T& r1; // 相当于1 OR 1,结果为1,即int& & r1 -> int& r1
T&& r2; // 相当于1 OR 1,结果为1,即int& && r2 -> int& r2
using U = int &&;
U& r3; // 相当于0 OR 1,结果为1,即int&& & r3 -> int& r3
U&& r4; // 相当于0 OR 0,结果为0,即int&& && r4 -> int&& r4
// 这个函数返回一个ValueGuard对象,该对象保存着对trie中值的引用。
// 如果键不存在于树中,它将返回std::nullopt。
template <class T>
auto Get(std::string_view key) -> std::optional<ValueGuard<T>>;
std::optional
可以隐式转换为boolean类型来表示当前是否有有效值,同时optional支持操作符*来进行取值。从这个例子中我们可以看出来,用optional作为函数返回值可以更好地解决引言中的问题,函数更简洁同时接口含义也更明确。
trie_store.cpp
实现TrieStore类,该类属性:
private:
// 这个互斥锁保护根节点。每次要访问或修改树的根目录时,都需要使用此锁。
std::mutex root_lock_;
// 这个互斥锁对所有写操作进行排序,一次只允许一个写操作。
std::mutex write_lock_;
// Stores the current root for the trie.
Trie root_;
互斥量std::mutex
可直接使用root_lock_.lock()
和root_lock_.unlock()
/// 一个特殊类型,它将阻塞move构造函数和move赋值操作符。用于TrieStore测试。
class MoveBlocked {
public:
explicit MoveBlocked(std::future<int> wait) : wait_(std::move(wait)) {}
MoveBlocked(const MoveBlocked &) = delete;
MoveBlocked(MoveBlocked &&that) noexcept {
if (!that.waited_) {
that.wait_.get();
}
that.waited_ = waited_ = true;
}
auto operator=(const MoveBlocked &) -> MoveBlocked & = delete;
auto operator=(MoveBlocked &&that) noexcept -> MoveBlocked & {
if (!that.waited_) {
that.wait_.get();
}
that.waited_ = waited_ = true;
return *this;
}
bool waited_{false};
std::future<int> wait_;
};
std::future
用于异步处理,std::async
异步地执行传入的函数并返回std::future
,执行结束前使用std::futrue.get()
可以尝试获取执行结果,该方法会阻塞当前线程直到结果可用。
noexcept
编译期完成声明和检查工作.noexcept
主要是解决的问题是减少运行时开销. 运行时开销指的是, 编译器需要为代码生成一些额外的代码用来包裹原始代码,当出现异常时可以抛出一些相关的堆栈stack unwinding
错误信息, 这里面包含,错误位置, 错误原因, 调用顺序和层级路径等信息.当使用noexcept
声明一个函数不会抛出异常候, 编译器就不会去生成这些额外的代码, 直接的减小的生成文件的大小, 间接的优化了程序运行效率.c++ 中的关键字noexcept - 知乎 (zhihu.com)