2022 CMU15445 Project0

前置知识

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构建,不要先构建再拷贝或移动。

    1. 当参数是左值时,push_backemplace_back的区别在于后者对参数多进行了一个std::forward操作

    2. 当参数是右值时,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实现:

  1. 首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。(cpr:先能够把参数类型全都接收)

  2. 然后我们通过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;
 }

模板中引用折叠的规则:

  1. T& & –> T&。

  2. T&& & –> T&。

  3. T& && –>T&。

  4. 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.背景

假如我们封装了一个操作,主要是用来创建对象使用(类似设计模式中的工厂模式),这个操作如下:

  1. 可以接受不同类型的参数,然后构造一个对象的指针。

  2. 性能尽可能高。

现在假设这个类的定义如下:

 #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; //错误
  1. 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.

  1. 总结

完美转发使用两步来完成任务:

  1. 在模板中使用&&接收参数。

  2. 使用std::forward()转发给被调函数.

这样左值作为仍旧作为左值传递,右值仍旧作为右值传递!

探讨

unique_ptr参数传递是传右值引用

unique_ptr<T>&& rvalue

还是传值

unique_ptr<T> rvalue

有什么区别?

  • 参考知乎讨论贴

    结论

    unique pointer作为函数参数的综合结论表

    名称

    函数接口形式

    结论
    By valuecallee(unique_ptr<Widget> smart_w)很好Good,sink自动发生,编译器保证安全。需要注意:caller必须用std::move()
    By non-const l-value referencecallee(unique_ptr<Widget> &smart_w)可以用,但实战中应该几乎不会出现,建议你仔细检查callee代码
    By const l-value referencecallee(const unique_ptr<Widget> &smart_w)最好不用,推荐用raw pointer会更清晰。需要注意:不要害怕在smart pointer里用到raw pointer,这并不存在资源泄漏问题
    By r-value referencecallee(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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值