目录
写在前面
需求
分析
接口设计
项目实现
一些思考与总结
致谢
写在前面
刚刚介绍了变参模板和完美转发,现在换一换脑子做一个小的项目实战吧。博主最近学习的是标准库,总体来说,我认为标准库中的内容是很trivial的,重点还是在有需求的时候能够利用好编译器和cppreference。博主也不准备逐个总结各种标准库中数据结构的使用方法。因为是标准库,所以其实方法大同小异,更多是一个熟练掌握的过程。此外标准库的写法也是一种很好的规范,非常值得我们借鉴学习。今天带来的这个项目也采用了类似的规范,最终实现了一个通用的容器能够插入不同类型的对象并实现最大最小值的获取。在代码编写过程对标准库中晦涩难懂的类型进行了跳转最终找到了其原始的基本类型,这种操作对于标准库的理解十分有益,希望大家在进行相关练习时也要善于阅读源码。希望大家共同坚持共同进步~
需求:
-
实现一个通用容器能够插入不同的类型和自定义结构体以及自定义类对象。(模板类)
-
能够根据不同的比较规则从容器中获取最大值或最小值。(基于红黑树的排序容器,set, map, multi-map)
分析:
-
通用容器,自己开发还是使用或继承stl标准库中的数据结构?
-
支持多中数据存储,模板类。
-
取最大最小值,使用函数对象或者使用有排列属性的数据结构(set/map)。
接口设计(sizeFilter)
-
构造函数,析构函数,拷贝构造,拷贝赋值。
-
插入和删除。
-
查找最大最小值。
项目实现
-
我们这一次采用类似于标准模板库中的编码规范来编写我们的容器,类内成员变量名称前面加一个下划线。
-
便于方便我们把类接口的实现全部写在头文件中。
-
首先我们的类是一个模板类,在我们的模板中会包含一个stl中的数据结构,默认是std::set<>。
-
我们的类中应该包含一个容器进行数据的存储和排序,这个数据成员被设为保护权限,这个容器的类型在模板中声明。
-
按照这个逻辑我们编写出我们类的定义。
-
/* * Created by herryao on 1/29/24. * Email: stevenyao@g.skku.edu * Sungkyunkwan Univ. Nano Particle Technology Lab(NPTL) * this is a project for achieving a container, * enabling inserting a variety type of members, * including the structs or classes defined by users, * having the function accessing the maximum or minimum members within, * corresponding with their regulation of comparison. * * this project is following the coding style of the data structure stack in the stl library */ #ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { protected: _Container _c; }; #endif//PROJECT01_SIZEFILTER_H
-
分别定义默认构造,有参构造,拷贝构造,拷贝赋值以及一个默认的析构函数。
-
在拷贝赋值的定义时发现返回类型
sizeFilter<_Ty, _Container>
非常冗长,因此采用typedef
对这类复杂的类型名称进行重命名来增加代码可读性。 -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: typedef sizeFilter<_Ty, _Container> Myt; //directly initialized with one empty constructor sizeFilter():_c(){} //destructor defined as default since no memory allocated from heap ~sizeFilter()=default; //copy constructor //sizeFilter<_Ty, _Container>& _Right is too long //sizeFilter(const sizeFilter<_Ty, _Container>& _Right):_c(_Right._c){} sizeFilter(const Myt& Right):_c(Right._c){} //constructor using a specified container explicit sizeFilter(const _Container& Cont):_c(Cont._c){} //copy assignment //using reference return type for A=B=C Myt& operator = (const Myt& Right){ if(this != &Right){ this->_c = Right._c; } return *this; } protected: _Container _c; }; #endif//PROJECT01_SIZEFILTER_H
-
结束这些基本的类内重要函数方法的定义我们首先来实现一些简单的接口,首先是容器是否为空,这个很简单只需要调用类型中的方法并返回一个布尔值即可。
-
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //a function check if the current container is empty [[nodiscard]] bool empty()const{ return (this->_c.empty()); } }; #endif//PROJECT01_SIZEFILTER_H
-
然后是获取当前容器内元素的个数,我们可以直接调用成员的size()方法,这个size()方法返回的是一个size_type类型。我们首先看一下这个类型在std::set中的定义。
-
/// Returns the size of the %set. size_type size() const _GLIBCXX_NOEXCEPT { return _M_t.size(); }
-
跳进去可以看到其定义如下,实际上是一个重命名,大概可以看出是定义在另外一个类里面的一个成员类型。
-
public: ///@{ /// Iterator-related typedefs. // _GLIBCXX_RESOLVE_LIB_DEFECTS // DR 103. set::iterator is required to be modifiable, // but this allows modification of keys. typedef typename _Rep_type::size_type size_type;
-
我们继续跳入到
_Rep_type
这个类中看一下这个size_type到底是个什么东西。 -
private: typedef _Rb_tree<key_type, value_type, _Identity<value_type>, key_compare, _Key_alloc_type> _Rep_type;
-
原来这个
_Rep_type
又是一个别名,别慌,我们继续跳进这个_Rb_tree
中看一看: -
template<typename _Key, typename _Val, typename _KeyOfValue, typename _Compare, typename _Alloc = allocator<_Val> > class _Rb_tree
-
可以看到我们终于跳出了这个重命名进入了一个类,现在我们开始重新搜索一下这个
size_type
,定位到他的定义处。 -
public: typedef size_t size_type;
-
终于找到了,原来这个
size_type
其实就是一个size_t的重命名,这种重命名的用法在标准库中被大量使用,因此也提升了标准库的阅读难度,但是实际上标准库也是基于cpp的基本语法,因此只要能捋清头绪最终都会找到cpp中的关键字的。
-
-
现在我们了解了这个类型,但是我们还是不要破坏标准库中的书写习惯,既然是标准库中封装好了的命名,我们就直接使用也方便用户去跳转,但是我们也可以参照他们的方法对这个类型进行一个重写。
-
顺便直接提及一下,除了这个size()返回的是
size_type
以外,insert()
,erase()
等方法也需要传入另外一个标准库中重命名的类型value_type
大家可以自行跳转看一下这个类型是什么,博主这里就不过多赘述了。 -
由于想要使用这两个返回类型,我们每次都要写
typename _Container::size_type
这么长的一个类型转换(注意:这里由于size_type本质是一个类型,而不是一个成员。此外_Container
是我们传入的一个模板的类,其本质也是一个模板。所以typename
关键字在这里是必须的,因为_Container::size_type
是一个依赖于模板参数的类型,所以需要typename
来指明它是一个类型名称。)所以所有的类型重命名实现如下: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: typedef sizeFilter<_Ty, _Container> Myt; //if try to access the type name in a template class, the keyword typename is needed //otherwise the compiler cannot identify whether this is a typename or a // member static variable or something else. typedef typename _Container::size_type size_type; typedef typename _Container::value_type value_type; };
-
现在我们可以用我们上面重命名的名称来简化我们的代码了,
size()
方法的实现如下,其余部分就略过了: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //access the size of the container size_type size()const{ return this->_c.size(); } };
-
现在我们来实现一个插入的接口,首先我们已经定义了传入参数类型的重命名
value_type
,其次我们知道set这个类的insert方法会返回一个std::pair<std::set::iterator, bool>
的一个对象,前面表示插入位置的迭代器,后面表示插入是否成功,因此我们可以定义一个临时对象并同过其第二元素来判断插入操作是否成功(此时仅限于set
,对于multiset
会有一些区别,稍后会进行解释) -
下面是插入接口的声明和实现
-
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: bool insert(const value_type& Val){ //return type of the method insert is a pair<set<_Ty>::iterator, bool>, //where the first member is the iterator pointing to the inserted value if successful //while, second member indicated if the insert operation is successful or not //hereby once more, the _Container is a template class, as a result, as mentioned before, //the type name need to be explicitly declared using typename to inform compiler that the //keyword here is a type name rather than anything else std::pair<typename _Container::iterator, bool> ret = this->_c.insert(Val); if(ret.second){ std::cout << "succeed in inserting operation on " << Val << std::endl; }else{ std::cout << "failed in inserting operation" << std::endl; } return ret.second; } }; #endif//PROJECT01_SIZEFILTER_H
-
然后是相应的
erase()
方法的实现,同样地要传入一个value_type
类型的数据并调用容器成员的erase()
方法对其进行删除,值得注意的是标准库这个方法会返回删除内容的个数,因此只要个数大于一就可以认为删除操作是成功的。我们对这个方法进行实现: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //and delete bool erase(const value_type& Val){ //there is one member function in the std::set erase() //which will return an integer indicating the number of keys deleted //if the returned value is larger than 1, indicating a successful operation if(this->_c.erase(Val) > 0){ std::cout << "succeed in deleting operation" << std::endl; return true; }else{ std::cout << "failed in deleting operation" << std::endl; return false; } } }; #endif//PROJECT01_SIZEFILTER_H
-
接下来是
clear()
的接口实现,很简单直接调用即可: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: void clear(){ this->_c.clear(); } }; #endif//PROJECT01_SIZEFILTER_H
-
然后就是我们的关键接口,获取最大值和最小值。首先我们思考一下,当容器中有元素,第一个数据就是最小值,最后一个数据就是最大值我们直接按照需求进行返回即可。但是如果这个容器是空的我们该如何返回呢?
-
可以借助标准库中的思路返回一个
std::pair
的对象,将寻找的成功与否和寻找到的元素一并返回,于是我们可以实现: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //access the minimum value in the container //how to achieve the return? if there is a value, return the value, else return false? std::pair<value_type, bool> getMin()const{ std::pair<value_type, bool> ret; if(!this->_c.empty()){ typename _Container::iterator pmin = _c.begin(); ret.first = *pmin; ret.second = true; }else{ ret.second = false; } return ret; } }; #endif//PROJECT01_SIZEFILTER_H
-
接下来用类似的思路我们来实现一下
getMax()
这个接口,一样地返回一个pair对象。 -
但是由于我们最大值在最后,而set不支持
.back()
,所以我们需要用.end()
来偏移获取最后一个数据的迭代器,即最大值的迭代器。 -
这时候便出现了一个问题那就是如何获取?简单的想法就是获取迭代器后-1,但是-1这种操作只适合于顺序容器如
std::vector
,std::deque
,std::list
等。但是std::set
,std::multiset
,std::map
,std::multimap
等关联容器(基于树或者哈希表实现),不支持这种+1, -1的操作只支持++或者--。所以我们的getMax()的实现如下: -
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //access the maximum value in the container //how to achieve the return? following the definition in the getMin std::pair<value_type, bool> getMax()const{ std::pair<value_type, bool> ret; if(!this->_c.empty()){ //typename _Container::iterator pmax = _c.end()-1; //typename _Container::iterator pmax = _c.end()--; //to be noticed here, the iterator in the set is a bidirectional iterator // instead of a random access iterator, try not to use std::set<_Ty>::iterator it = _c.end() - 1; //besides _c.end() returns a temporary object, meaning that it is illegal to do -- operation directly on which, //in another word, the .end() function returns a right value instead of a left value. //right way is first store it in a variable and then perform the --operation typename _Container::iterator pmax = _c.end(); ret.first = *(--pmax); ret.second = true; }else{ ret.second = false; } return ret; } }; #endif//PROJECT01_SIZEFILTER_H
-
至此一个基于set的通用容器完成,现在我们开始做一些测试。
-
首先测试一下最大最小值的获取以及清空操作
-
创建一个容器对象,在容器内部添加五个元素,此时会调用insert方法并会有一些输出信息。
-
然后获取一下最大值,如果有最大值就输出否则打印获取失败。
-
然后获取一下最小值,如果有最小值就输出否则打印获取失败。
-
清除掉容器中的元素。
-
然后再插入一个元素。
-
继续搜索最大最小值,此时应当均有输出且输出一样的结果。
-
-
#include <iostream> #include "sizeFilter.h" void test_4_max_min(){ sizeFilter<int> sf; for(int i=0; i<5; ++i){ sf.insert(5*i); } std::cout << "get the result here" <<std::endl; auto ret_max = sf.getMax(); if(ret_max.second){ std::cout << "find max val: " << ret_max.first << std::endl; }else{ std::cout << "failed in find max" << std::endl; } auto ret_min = sf.getMin(); if(ret_min.second){ std::cout << "find min val: " << ret_min.first << std::endl; }else{ std::cout << "failed in find min" << std::endl; } sf.clear(); std::cout << "after clear" <<std::endl; sf.insert(5); auto ret_max_1 = sf.getMax(); if(ret_max_1.second){ std::cout << "find max val: " << ret_max_1.first << std::endl; }else{ std::cout << "failed in find max" << std::endl; } auto ret_min_1 = sf.getMin(); if(ret_min_1.second){ std::cout << "find min val: " << ret_min_1.first << std::endl; }else{ std::cout << "failed in find min" << std::endl; } } int main() { test_4_max_min(); return 0; }
-
测试结果如下,完全符合预期:
-
D:\ClionProject\project\cmake-build-debug\project.exe succeed in inserting operation on 0 succeed in inserting operation on 5 succeed in inserting operation on 10 succeed in inserting operation on 15 succeed in inserting operation on 20 get the result here find max val: 20 find min val: 0 succeed in inserting operation on 5 after clear find max val: 5 find min val: 5 Process finished with exit code 0
-
然后再测试一下删除操作:
-
类似上一个操作添加五个元素。
-
删除一个存在的元素。
-
删除一个不存在的元素。
-
-
#include <iostream> #include "sizeFilter.h" void test_4_erase(){ sizeFilter<int> sf; for(int i=0; i<5; ++i){ sf.insert(5*i); } std::cout << "get the result here" <<std::endl; std::cout << "erase a exist value: " << std::endl; sf.erase(5); std::cout << "erase one unknown value: " << std::endl; sf.erase(60); } int main() { test_4_erase(); return 0; }
-
相应的输出结果如下,存在和不存在的元素都得到了相应的处理:
-
D:\ClionProject\project\cmake-build-debug\project.exe succeed in inserting operation on 0 succeed in inserting operation on 5 succeed in inserting operation on 10 succeed in inserting operation on 15 succeed in inserting operation on 20 get the result here erase a exist value: succeed in deleting operation erase one unknown value: failed in deleting operation Process finished with exit code 0
-
接下来是重头戏了,我们来更换一下基本类型,使用一个std::multiset吧
-
void test_4_other_container(){ sizeFilter<int, std::multiset<int>> sf; sf.insert(1); //return a pair while insert one value only std::set<int> st; auto ret_multiset = st.insert(1); std::multiset<int> ms; //the return type of multiset insert using one value is iterator only // while without a bool type and not a pair. auto ret_multiset = ms.insert(1); }
-
当直接传入一个int的数据时,会报错一个类型不匹配,这是为什么呢?
-
====================[ Build | project01 | Debug ]=============================== /home/herryao/Software/clion-2023.2/bin/cmake/linux/x64/bin/cmake --build /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/cmake-build-debug --target project01 -- -j 10 [ 50%] Building CXX object CMakeFiles/project01.dir/main.cpp.o In file included from /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/sizeFilter.hpp:7, from /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/main.cpp:2: /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/sizeFilter.h: In member function ‘bool sizeFilter<_Ty, _Container>::insert(const value_type&)’: /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/sizeFilter.h:78:5: warning: no return statement in function returning non-void [•]8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wreturn-type•-Wreturn-type•]8;;•] 78 | } | ^ /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/main.cpp: In function ‘void test_4_other_container()’: /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/main.cpp:65:10: error: conflicting declaration ‘auto ret_multiset’ 65 | auto ret_multiset = ms.insert(1); | ^~~~~~~~~~~~ /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project01/main.cpp:61:10: note: previous declaration as ‘std::pair<std::_Rb_tree_const_iterator<int>, bool> ret_multiset’ 61 | auto ret_multiset = st.insert(1); | ^~~~~~~~~~~~ gmake[3]: *** [CMakeFiles/project01.dir/build.make:76: CMakeFiles/project01.dir/main.cpp.o] Error 1 gmake[2]: *** [CMakeFiles/Makefile2:83: CMakeFiles/project01.dir/all] Error 2 gmake[1]: *** [CMakeFiles/Makefile2:90: CMakeFiles/project01.dir/rule] Error 2 gmake: *** [Makefile:124: project01] Error 2
-
原来是因为multiset直接插入一个数据会返回一个迭代器,而set的insert会返回一个pair<set<_Ty>::iterator, bool>,这样我们写的方法就会出现匹配的问题。那么简单的解决方法就是寻找一个共性的方法,即相同的参数和相同的返回值,于是在cppreference 中我们可以看到有两个方法是完全一致的,但是这里我们选择传入一个左值的常引用来完成我们的一致性代码。
-
现在只需要把这个地方变成在容器首地址插入即可(因为是树状结构,会自动排序因此在哪里插入是不影响的)修改后的插入方法接口实现如下:
-
#ifndef PROJECT01_SIZEFILTER_H #define PROJECT01_SIZEFILTER_H #include <iostream> #include <set> template<typename _Ty, class _Container = std::set<_Ty>> class sizeFilter { public: //insert bool insert(const value_type& Val){ //return type of the method insert is a pair<set<_Ty>::iterator, bool>, //where the first member is the iterator pointing to the inserted value if successful //while, second member indicated if the insert operation is successful or not //hereby once more, the _Container is a template class, as a result, as mentioned before, //the type name need to be explicitly declared using typename to inform compiler that the //keyword here is a type name rather than anything else //previous method is commented, which is not suitable for std::multiset /* std::pair<typename _Container::iterator, bool> ret = this->_c.insert(Val); if(ret.second){ std::cout << "succeed in inserting operation on " << Val << std::endl; }else{ std::cout << "failed in inserting operation" << std::endl; } return ret.second; */ bool ret = false; //for utilizing the multiset typename _Container::iterator flag = _c.insert(_c.begin(), Val); if(flag != this->_c.end()){ std::cout << "succeed in inserting operation on " << Val << std::endl; ret = true; }else{ std::cout << "failed in inserting operation" << std::endl; } return ret; } }; #endif//PROJECT01_SIZEFILTER_H
-
重新运行之前的测试结果如下:
-
D:\ClionProject\project\cmake-build-debug\project.exe succeed in inserting operation on 1 succeed in inserting operation on 5 succeed in inserting operation on 7 the size of the container is 3 the maximum value is 7 the minimum value is 1 Process finished with exit code 0
-
可以看出insert方法执行一切正常。
一些思考与总结
-
此项目实战综合了标准库中的一些通用的方法如
insert()
,clear()
,size()
。 -
参照标准库中的写法对一些冗余的类型名称进行了重命名。
-
对于模板类中自定义的一些类型的使用需要在前面加上
typename
关键字。 -
关联类型容器的迭代器支支持++, -- 操作。
-
通用方法的函数接口未必一样,应该尽可能考虑 不同类型的使用,因此在设计程序之前,阅读相关文档是非常必要的。
致谢
-
感谢各位的支持,希望大家的cpp水平不断变强。
-
感谢Martin老师的课程。