前置知识
push_back() 和emplace_back()有什么区别?
-
emplace_back避免额外的移动和拷贝操作。
1.1 异同点 如果参数是左值,两个调用的都是copy constructor 如果参数是右值,两个调用的都是move constructor(C++ 11后push_back也支持右值) 最主要的区别是,emplace_back支持in-place construction,也就是说emplace_back(10, “test”)可以只调用一次constructor,而push_back(MyClass(10, “test”))必须多一次构造和析构 1.2 需要澄清的一些误解 emplace_back的效率比push_back高,无论什么情况下都高,所以可以无脑用。从上面1、2点可以看到,两者其实没有区别 push_back不支持右值参数,不能调用move constructor,效率低。由C++ Reference可以得知,C98是没有右值形参的,但C++11已经增加了。 emplace_back的优势是右值时的效率优化。这是最大的误解,emplace_back的最大优势是它可以直接在vector的内存中去构建对象,不用在外面构造完了再copy或者move进去!!! 1.3 使用建议 左值用push_back 右值用emplace_back 局部变量尽量使用emplace_backin-place构建,不要先构建再拷贝或移动。
-
当参数是左值时,
push_back
和emplace_back
的区别在于后者对参数多进行了一个std::forward
操作 -
当参数是右值时,
push_back
其实是调用的emplace_back
实现的。
-
智能指针 std::unique_ptr<T>()
-
C++ Primer 智能指针 12.1.5
-
unique_ptr 独占指针
-
只能使用
std::move()
转移拥有权
-
左值引用,右值引用
3.1 左值和右值的概念
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体; 右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。 一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。 左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。 右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。
3.2 左值引用和右值引用
引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。引用可以改变指针的指向,还可以改变指针所指向的值。
-
左值引用:type &引用名 = 左值表达式;就是对左值的引用 就是给左值取别名
-
右值引用:type &&引用名 = 右值表达式;就是对右值的引用 就是给右值取别名
int a=10; //a 是左值 double b=1.3; //b 是左值 //左值引用 int & Ta=a; //引用左值 故 是一个左值引用 double & Tb=b; //引用左值 故是一个左值引用
再比如:
int a = 100; int&& b = 100;//右值引用 int& c = b; //正确,b为左值 int& d = 100; //错误,100为右值,无法引用
移动语义 std::move()
-
C++ Primer 13.6移动语义以及16.2.6
-
右值引用:只能绑定到一个将要销毁的对象
-
右值引用也不过是某个对象的另一个名字而已。
-
std::move()
显式地将左值转换为右值引用,头文件utility
中 -
从一个左值
static_cast
到一个右值是允许的
在C++11中,标准库在中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
std::move 的函数原型定义:
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type &&>(t); }
首先,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景)。关于引用折叠如下:
所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&) 。 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)。
*简单来说,右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用。*
使用remove_reference进行引用折叠
//原始的,最通用的版本 template <typename T> struct remove_reference{ typedef T type; //定义T的类型别名为type }; //部分版本特例化,将用于左值引用和右值引用 template <class T> struct remove_reference<T&> //左值引用 { typedef T type; } template <class T> struct remove_reference<T&&> //右值引用 { typedef T type; } //举例如下,下列定义的a、b、c三个变量都是int类型 int i; remove_refrence<decltype(42)>::type a; //使用原版本, remove_refrence<decltype(i)>::type b; //左值引用特例版本 remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
4.3 std::move实现:
-
首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。(cpr:先能够把参数类型全都接收)
-
然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T(模板偏特化)。(cpr:再把接收的参数的原引用抹除强转成右引)
4.4 std::move的优点
std::move语句可以将左值变为右值而避免拷贝构造。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
4.5 std::move的使用
比如向vector中插入新元素。
int a = 1; vector<int> vec; vec.push_back(move(a));
引用折叠
*引用折叠只能应用于推导的语境下。*(如:模板实例化,auto,decltype等)
*右值引用加上右值引用等于右值引用(&&+&&==&&),除了这个外其他的所有形式都为左值引用。*
template<typename T> CData* Creator(T&& t) //t与&&进行引用折叠 { return new CData(t); } void Forward() { const char* value = "hello"; std::string str1 = "hello"; std::string str2 = " world"; CData* p = Creator(str1 + str2); delete p; }
模板中引用折叠的规则:
-
T& & –> T&。
-
T&& & –> T&。
-
T& && –>T&。
-
T&& && –> T&&。
四种类型转换
-
static_cast
使用最多 -
reinterpret_cast
任何类型 -
const_cast
去掉const -
dynamic_cast
继承关系间,指针和引用的互相转换
完美转发 std::forward
-
C++ Primer 16.2.7 转发
-
std::forward
将参数连同类型转发给其他函数,保留const和左值,右值属性。
foo(std::forward<Type>(args));
1.背景
假如我们封装了一个操作,主要是用来创建对象使用(类似设计模式中的工厂模式),这个操作如下:
-
可以接受不同类型的参数,然后构造一个对象的指针。
-
性能尽可能高。
现在假设这个类的定义如下:
#include <iostream> #include <string> using namespace std; class CData { public: CData() = delete; CData(const char* ch) : data(ch) { std::cout << "CData(const char* ch)" << std::endl; } CData(const std::string& str) : data(str) { std::cout << "CData(const std::string& str)" << std::endl; } CData(std::string&& str) : data(str) { std::cout << "CData(std::string&& str)" << std::endl; } ~CData() { std::cout << "~CData()" << std::endl; } private: std::string data; };
这里如果需要高效率,对于右值的调用应该使用CData(std::string&& str) : data(str)移动函数操作。
1.1 普通模板定义
如果只要一个函数入口来创建对象,那么使用模板是不错的选择,例如可以如下:
template<typename T> CData* Creator(T t) { return new CData(t); } void Forward() { const char* value = "hello"; std::string str1 = "hello"; std::string str2 = " world"; //CData* p = Creator(value); CData* p = Creator(str1); //CData* p = Creator(str1 + str2); delete p; }
这种办法虽然行得通,但是比较挫,因为每次调用Creator(T t)
的时候,都需要拷贝内存,明显不满足高效的情况。
1.2 引用模板定义
上面说的值拷贝不能满足内容,那么我们使用引用就可以解决问题了吧?
template<typename T> CData* Creator(T& t) { return new CData(t); } void Forward() { const char* value = "hello"; std::string str1 = "hello"; std::string str2 = " world"; CData* p = Creator(value); //CData* p = Creator(str1); delete p; }
但是这种情况对于CData* p = Creator(str1 + str2)
无法解决问题,因为右值无法赋值到左值的引用。
1.3 右值引用模板
从上面我们比较容易想到,使用&&来解决拷贝的问题。
template<typename T> CData* Creator(T&& t) { return new CData(t); } void Forward() { const char* value = "hello"; std::string str1 = "hello"; std::string str2 = " world"; CData* p = Creator(str1 + str2); delete p; }
由于模板中引用折叠的规则:
T& & –> T&。 T&& & –> T&。 T& && –>T&。 T&& && –> T&&。 这种使用基本能够满足使用了。但是真的吗?别急我们来分析一下效率问题。
对于CData* p = Creator(str1 + str2);的调用:
产生一个右值str1 + str2. CData* Creator(T&& t)右值引用,此时为:std::string&& t。 那么return new CData(t);为右值引用,调用构造函数的CData(std::string&& str) : data(str)移动构造函数。 但是是否是这样的呢?如果是这样效率确实是最佳的,因为我们只需要右值直接移动就是最高效率了,运行结果如下:
CData(const std::string& str) ~CData()
显然并没有调用移动函数,原因是因为在函数:
CData* Creator(T&& t) { return new CData(t); }
t 是一个变量,为左值,无论左值引用类型的变量还是右值引用类型的变量,都是左值,因为它们有名字。,例如可以写如下代码:
int a = 100; int&& b = 100; int& c = b; //正确,b为左值 int&d = 100; //错误
-
forward完美转发
我们使用fordward来完美解决这个问题。
#include <memory> #include <iostream> #include <string> class Entity{ public: Entity(){ std::cout<<"constructor\n"; } void print(){ std::cout<<"smart boy!\n"; } ~Entity(){ std::cout<<"destructor\n"; } }; void foo(std::unique_ptr<Entity> temp){ std::cout<<"foo \n"; temp->print(); } int main() { // 1. p.get()慎用,返回p中保存的指针 // 2. p->mem 等价于 (*p).mem // 3. *p 解引用,获得它指向的对象 std::cout<<"main\n"; { // 创建只支持new // 不支持p2(p1) // 不支持p2 = p1 std::unique_ptr<Entity> p(new Entity()); //foo(p); foo(std::move(p)); } std::cout<<"gg\n"; return 0; }
foo(p);错误
-
unique_ptr 独占指针
-
只能使用
std::move()
转移拥有权
-
2.1 完美转发模板
template<typename T> CData* Creator(T&& t) { return new CData(std::forward<T>(t)); } void Forward() { const char* value = "hello"; std::string str1 = "hello"; std::string str2 = " world"; CData* p = Creator(str1 + str2); delete p; }
此时运行结果为:
CData(std::string&& str) ~CData()
2.2 结论
所谓的完美转发,是指std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
这个对于上面一个例子带来的好处就是函数转发仍旧为右值引用,可以使用移动函数。
2.3 原理
std::forward
定义如下:
template<class _Ty> _NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue return (static_cast<_Ty&&>(_Arg)); } CData* Creator(T&& t) { return new CData(std::forward<T>(t)); }
如果T为std::string&,那么std::forward<T>(t) 返回值为std::string&&&, 折叠为std::string&,左值引用特性不变。 如果T为std::string&&,那么std::forward<T>(t) 返回值为std::string&&&&, 折叠为std::string&&,右值引用特性不变。 如果调用者为std::string,调用将会转换成为std::string&,为类型1.
-
总结
完美转发使用两步来完成任务:
-
在模板中使用&&接收参数。
-
使用
std::forward()
转发给被调函数.
这样左值作为仍旧作为左值传递,右值仍旧作为右值传递!
探讨
unique_ptr参数传递是传右值引用
unique_ptr<T>&& rvalue
还是传值
unique_ptr<T> rvalue
有什么区别?
-
结论
unique pointer作为函数参数的综合结论表
名称 函数接口形式
结论 By value callee(unique_ptr<Widget> smart_w) 很好Good,sink自动发生,编译器保证安全。需要注意:caller必须用std::move() By non-const l-value reference callee(unique_ptr<Widget> &smart_w) 可以用,但实战中应该几乎不会出现,建议你仔细检查callee代码 By const l-value reference callee(const unique_ptr<Widget> &smart_w) 最好不用,推荐用raw pointer会更清晰。需要注意:不要害怕在smart pointer里用到raw pointer,这并不存在资源泄漏问题 By r-value reference callee(unique_ptr<Widget> &&smart_w) 对于unique pointer,右值引用和Copy by value几乎等效
全字典树分为三部分
TrieNode | 字典树结点 |
---|---|
TrieNodeWithValue | 继承字典树节点,用于放尾值 |
Trie | 字典树 |
TrieNode
成员变量
char key_char_; | 此trie节点的关键字符 |
---|---|
bool is_end_{false}; | 标记是否是尾结点 |
std::unordered_map<char, std::unique_ptr<TrieNode>> children_; | 此trie节点的所有子节点的映射,每个子节点都可以访问 |
成员函数
TrieNode(TrieNode &&other_trie_node) noexcept {} | 构造函数 |
---|---|
virtual ~TrieNode() | 析构函数 |
bool HasChild(char key_char) const | 该结点是否有对应子结点 |
bool HasChildren() const | 该结点是否拥有子节点 |
bool IsEndNode() const | 该结点是否是尾结点 |
char GetKeyChar() const | 获得该结点的关键字符 |
std::unique_ptr<TrieNode> *InsertChildNode | 插入子结点 |
std::unique_ptr<TrieNode> *GetChildNode(char key_char) | 获得对应子节点 |
void RemoveChildNode(char key_char) | 删除对应子节点 |
void SetEndNode(bool is_end) | 设置尾节点 |
InsertChildNode
-
如果该结点含有拥有key_char的子结点,返回nullptr
-
如果key_char和子节点拥有的key_char不一致,返回nullptr
-
child是右值,当将他插入unordered_map中时应该使用std::forward()移动他,这样可以避免传入参数后被当成左值,移动语义
-
返回值是指向unique_ptr的指针,因为指向unique_pr的指针可以访问底层数据,而无需拥有unique_ptr。
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]; }
TrieNodeWithValue
成员变量
T value_; | 存储值 |
---|---|
TrieNode的成员变量 | 继承来的 |
成员函数
-
此处构造函数先构造TrieNode,再构造TrieNodeWithValue
-
此处构造TrieNode也采用移动语义,因为TrieNodeWithValue仅用来替换TrieNode,替换后trieNode再无他用。
TrieNodeWithValue(TrieNode &&trieNode, T value) : TrieNode(std::forward<TrieNode>(trieNode)) { this->value_ = value; SetEndNode(true); }
Trie
成员变量
std::unique_ptr<TrieNode> root_ | 字典树根节点(相当于链表的dummy_head) |
---|---|
ReaderWriterLatch latch_ | 字典树的读写锁 |
成员函数
Trie() | 构造函数 |
---|---|
bool Insert(const std::string &key, T value) | 插入结点 |
bool Remove(const std::string &key) | 删除结点 |
T GetValue(const std::string &key, bool *success) | 获得值 |
Insert
字典树可以有多个子节点,插入子节点不会产生冲突
-
若key为空,返回false
-
遍历key的每个字符,将其依次插入到字典树中
-
当key对应迭代器--,此时key为最后一位字符,字典树插入/遍历结点置尾结点,此结点可能无子结点也可能已经创建过子结点
-
对尾节点进行特殊处理
-
若当前结点的子结点已存在
-
is_end_ = true(即是尾结点), 返回false
-
is_end_ = false,调用TrieNode::SetEndNode(),将其标记为尾结点,同时将该结点与TrieNodeWithValue进行替换(TrieNodeWithValue继承了过去创建的子节点的成员遍历,相当于对子结点加了一个Value,其他参数不变)
-
-
若当前结点的子结点不存在
-
创建和当前结点相同的子结点,再将该结点与TrieNodeWithValue进行替换
-
-
-
全程上写锁,也可考虑使用lock_guard(mutex)
Remove
-
若key为空,返回false
-
创建栈(std::stack<std::tuple<char, std::unique_ptr<TrieNode> *>>)
-
栈中存放的是(key的当前字符,存放当前字符子节点的父结点)
-
遍历字典树压栈,终止条件尾父节点没有子节点
-
开始删除,弹栈
-
若弹出的元素中保存的父节点没有子结点,删除。(运用递归的思想,从下往上删除)
-
全程上写锁,也可考虑使用lock_guard(mutex)
GetValue
const std::string &key | 字符串 |
---|---|
bool *success | 是否成功获得值 |
T 返回值 | Value |
-
初始化success为false
-
同步遍历key与字典树
-
若中途没有匹配结点,success为false,返回{}
-
若遍历到最后,则使用dynamic_cast<TrieNodeWithValue<T> *>将TrieNode强制转换为TrieNodeWithValue,因为value放在存放地址的最后,前面存放的是TrieNode,所以可以强制转换
-
全程上读锁,也可考虑使用lock_guard(mutex)