第四讲
1、一个万用的Hash Function
说明:
1.1 针对自定义类型的哈希函数的编写方式
自定义类型:
#include <functional>
class Customer {
...
};
针对自定义类型编写哈希函数的三种方式:
- 方式一:编写仿函数
//形式1
class CustomerHash {
public:
std::size_t operator()(const Customer& c) const {
return ...
}
};
//第二个模板参数传入的是类型
unordered_set<Customer, CustomerHash> custset;
- 方式二:自定义哈希函数,并将函数对象传入
//形式2
size_t customer_hash_func(const Customer& c) {
return ...
}
//第二个模板参数为函数类型
unordered_set<Customer, size_t(*)(const Customer&)> custset(20, customer_hash_func);
- 方式三:以
struct hash
偏特化形式实现 Hash Function
针对自定义的类型实现偏特化的Hash function,如下是对MyString
类型进行特化的版本
G4.9中所有的基本类型都有自己的hash function,G2.9中没有对string
类型进行特化。
1.2 哈希函数的具体设计
-
方案一:因为基本类型都具有自己的hash function,所以想着将Customer中的所有数据拆开为基本类型,然后对各种类型进行hash,最后结果合在一起:
class CustomerHash { public: std::size_t operator()(const Customer&) const { return std::hash<std::string>()(c.fname) + std::hash<std::string>()(c.lname) + std::hash<long>()(c.no); } };
这种方案可以使用,但是太过天真,后期可能会产生很多碰撞💥;
-
方案二:使用
variadic templates
(可变化的模板)class CustomerHash { public: std::size_t operator()(const Customer&) const { return hash_val(c.fname, c.lname, c.no); //将Customer的数据都放入 } }; template <typename... Types> //接受任意个数的模板参数,语法"typename..." inline size_t hash_val(const Types&... args) { size_t seed = 0; hash_val(seed, args...); return seed; } template <typename T, typename... Types> //可以接受任意个数的模板参数 inline void hash_val(size_t& seed, const T& val, const Types&... args) { hash_combine(seed, val); hash_val(seed, args...); //递归调用本身,拆分参数,每次都取一个参数进行hash_combine,直到取到只有一个元素调用hash_val(size_t&, const T&)函数 } //上面两个函数的区别是第一个实参的类型 #include <functional> template <typename T> inline void hash_combine(size_t& seed, const T& val) { //最终获得的seed就是Customer的hash-code seed ^= std::hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } template <typename T> inline void hash_val(size_t& seed, const T& val) { hash_combine(seed,val); }
上面代码中的
hash_combine
函数里的0x9e3779b9
是黄金比例:
-
上文的源代码:
-
G4.9版本下的Hash Function使用示例:
2、Tuple用例
tuple:一堆东西的组合,可以指定任意类型的任意个元素为一个整体结构。
- tuple使用示例:
说明:
- 源码
#include<iostream>
#include <cstring>
#include <complex>
#include <functional>
using namespace std;
int main() {
cout << "string, sizeof = " << sizeof(string) << endl;
cout << "double, sizeof = " << sizeof(double) << endl;
cout << "float, sizeof = " << sizeof(float) << endl;
cout << "int, sizeof = " << sizeof(int) << endl;
cout << "complex<double>, sizeof = " << sizeof(complex<double>) << endl;
//tuples
//create a four-element tuple
//- elements are initialized with default value (0 for fundamental types)
tuple<string, int, int, complex<double> > t;
cout << "sizeof = " << sizeof(t) << endl;
//create and initialized a tuple explicitly
tuple<int, float, string> t1(41, 6.3, "nico");
cout << "tuple<int, float, string>, sizeof = " << sizeof(t1) << endl;
//iterator over elements: 取出元素
cout << "t1:" << get<0>(t1) << " " << get<1>(t1) << " " << get<2>(t1) << endl;
//create tuple with make_tuple()
auto t2 = make_tuple(22, 44, "stacy"); //编译器会进行实参推导出类型
//assign second value in t2 to t1
get<1>(t1) = get<1>(t2);
//comparison and assignment
//- include type conversion from tuple<int, int, const char*>
//to tuple<int, float, string>
if (t1 < t2) { //compares value for value
cout << "t1 < t2" << endl;
} else {
cout << "t1 >= t2" << endl;
}
t1 = t2; //Ok, assigns value for value
cout << "t1 : " << t1 << endl; //必须重载操作符重载operator<<
tuple<int, float, string> t3(77, 1.1, "more light");
int i1;
float f1;
string s1;
tie(i1, f1, s1) = t3; //assigns values of t to i, f, and s ,取出t3的成分绑定到各个变量
typedef tuple<int, float, string> TupleType;
cout << tuple_size<TupleType>::value << endl; //yields 3,有3个成分
tuple_element<1, TupleType>::type f1 = 1.0; //yields float
typedef tuple_element<1, TupleType>::type T;
return 0;
}
- 为什么
tuple<stirng, int, int, complex<double>> t;
的sizeof(t)
是32, 而不是28(理论上将tuple中的类型大小加起来)呢? - 如下是参数数量可变的模板应用到tuple,variadic templates语法自动把n个参数分解为1和n-1,再把n-1分解为1和n-2,…,不断递归直到没有参数,所以variadic templates一定是有一个主体,有一个终止条件。
tuple继承了自己(每次成分减少一个),variadic template会自动将其处理为继承的关系,如上图右边所示。当参数为0个的时候,使用特化的版本template<> class tuple<>{};
。子类的大小 = 子类本身的数据 + 父类的数据,那么上面提到的问题2,按照这种想法应该就是28,而不应该是32,这个待思考🤔。图中的tail()
函数返回值this
本来指的是例子中的这三块(41、6.3和nico),但是经过转型后变成只指向上两块(6.3和nico)。
type traits
- G2.9版本的type traits
struct __true_type {};
struct __false_type {};
//泛化版本
template <class type>
struct __type_traits {
typedef __true_type this_dummy_member_must_be_first;
typedef __false_type has_trivial_default_constructor; //默认构造函数是否重要,默认情况下是重要的
typedef __false_type has_trivial_copy_constructor; //拷贝构造函数是否重要,默认情况下是重要的
typedef __false_type has_trivial_assignment_operator; //赋值操作是否重要,默认情况下是重要的
typedef __false_type has_trivial_destructor; //析构函数是否重要,默认情况下是重要的
typedef __false_type is_POD_type; //Plain Old Data,C中的structure,没有function,只有data
};
//特化版本
//如果明确知道上面提问的那些函数不重要,那么就可以为这个泛化版本写特化版本,来说明答案不是默认的答案,而是有自己的答案
//对于整数这种type,所提问的那些函数都不重要
template<> struct __type_traits<int> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type; //Plain Old Data
};
//特化版本
template<> struct __type_traits<double> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type; //Plain Old Data
};
//自定义的类型,可以照着这种写法自己实现自定义类型的特化版本,自己判断那些函数是否重要,自己回答
//比如“复数类”,没有指针,只有实部和虚部,可以不用写构造函数和析构函数,因为编译器有默认版本,所以那些函数都是不重要的。
__type_traits<Foo>::has_trivial_destructor //算法要通过__type_traits询问Foo:你有不重要的析构函数吗
- C++11版本的type traits
新版本不需要对自己的自定义类型实现特化版本,而且可以询问的东西更多了。
type traits的测试
#include <iostream>
using namespace std;
//global function template
template <typename T>
void type_traits_output(const T& x) {
cout << "\ntype traits for type : " << typeid(T).name() << endl;
cout << "is_void\t" << is_void<T>::value << endl; //结果为true或者false
cout << "is_integral\t" << is_integral<T>::value << end;
cout << "is_floating_point\t" << is_floating_point<T>::value << endl;
cout << "is_arithmetic\t" << is_arithmetic<T>::value << endl;
cout << "is_signed\t" << is_signed<T>::value << endl;
cout << "is_unsigned\t" << is_unsigned<T>::value << endl;
cout << "is_const\t" << is_const<T>::value << endl;
cout << "is_volatile\t" << is_volatile<T>::value << endl;
cout << "is_class\t" << is_class<T>::value << endl;
cout << "is_function\t" << is_function<T>::value << endl;
cout << "is_reference\t" << is_reference<T>::value << endl;
cout << "is_lvalue_reference\t" << is_lvalue_reference<T>::value << endl;
cout << "is_rvalue_reference\t" << is_rvalue_reference::value << endl;
cout << "is_pointer\t" << is_pointer<T>::value << endl;
cout << "is_member_pointer\t" << is_member_pointer<T>::value << endl;
cout << "is_member_object_pointer\t" << is_member_object_pointer<T>::value << endl;
cout << "is_member_function\t" << is_member_function<T>::value << endl;
cout << "is_fundamental\t" << is_fundamental<T>::value << endl;
cout << "is_scalar\t" << is_scalar<T>::value << endl;
cout << "is_object\t" << is_object<T>::value << endl;
cout << "is_compound\t" << is_compound<T>::value << endl;
... //如下的测试结果图中更多
}
说明:
- 以上是将 string 放入 type_traits_output 函数进行测试的结果;
- 类中如果有指针的时候就需要编写析构函数;当类要被作为基类的时候,需要写虚析构函数,但是因为 string 并不会被作为基类,所以析构函数不需要写成virtual;
class Foo {
private:
int d1, d2;
};
type_traits_output(Foo());
说明:
Foo
中没有 function,只有data,所以它是pod type;
class Goo {
public:
virtual ~Goo { }
private:
int d1, d2;
};
type_traits_output(Goo());
说明:
Goo
中有虚析构函数,所以has_virtual_destructor
结果是true;is_polymorphic
是否为多态,“多态”类是声明或继承了virtual 方法,Goo
中声明了 virtual function,所以它是多态的;
class Zoo {
public:
Zoo(int i1, int i2): d1(i1), d2(i2) { } //构造函数
Zoo(const Zoo&) = delete; //拷贝构造
Zoo(Zoo&&) = default; //move constructor
Zoo& operator=(const Zoo&) = default; //拷贝赋值
Zoo& operator=(const Zoo&&) = delete; //move assignment
virtual ~Zoo() { }
private:
int d1, d2;
};
type_traits_output(Zoo(1, 2));
说明:
- 因为自己编写了构造函数,所以没有默认构造函数;
- 拷贝构造函数被delete了,所以没有拷贝构造函数;
- …
说明:
- 复数只有实部和虚部,所以不用写虚析构函数,编译器会有默认的,这个析构函数是不重要的;
type traits的实现
- is_void
说明:
- 都是使用“模板”对类型做操作;
remove_cv
是先将关键字const和volatile拿掉;struct remove_const<_Tp const>
是偏特化,范围的偏特化;is_void
模板类先利用remove_cv
将const
和volatile
去除,然后进入__is_void_helper
,如果是void
,则返回真,否则返回假;
- is_integral
说明:
is_integral
判断是否为整数类型;is_integral
会先将const和volatile关键字拿掉,然后进入__is_integral_helper
中,根据类型不同进入不同的偏特化版本;
- is_class,is_union,is_enum, is_pod
说明:
- 蓝色的这些没有在C++标准库的源代码中,可能是编译器编译的时候整理出来的结果;
- is_move_assignable
说明:
- 和上面一样,
is_reference
已经不在C++标准库源代码中; - type traits就是一个类型萃取机;
3、cout
说明:
cout
是个对象,extern
表明外界可以使用它;- 通过cout输出对象,因为它对不同类型产生的对象进行了重载
operator<<
,如果没有出现在重载的类型中,就需要自己重载operator<<
- 标准库中对
<<
的操作符重载:
4、moveable元素对容器速度效能的影响
说明:
-
上面的是moveable 元素的测试,300万个元素放入vector中,调用了0次copy constructor,但是调用了7194303次move constructor。花费的时间是8547ms;
-
下面的是non-moveable元素的测试,同样300万个元素,放入vector中,调用了7194303次copy constructor,调用了0次move constructor,花费的时间是14235ms;
-
M c11(c1);
是拷贝构造;M c12(std::move(c1));
是move copy; -
不同类型的容器调用相同的一份代码测试插入300万个元素:
for (long i = 0; i < value; ++i) { snprintf(buf, 10, "%d", rand()); auto ite = c1.end(); c1.insert(ite, V1type(buf)); }
-
vector的特性是当空间不够用的时候,就要2倍增长空间,并且将原来的数据拷贝到新的空间,这个过程会调用拷贝构造或拷贝赋值,因此,多出了很多的构造函数的调用;
说明:
- 不同于vector,list没有扩充或增长的行为,放300万个元素就调用300万次构造函数;
- 后面的容器也是相同的行为模式
写一个moveable class
源码:
#include <iostream>
#include <cstdio> //snprintf()
#include <cstdlib> //RAND_MAX
#include <cstring> //strlen(), memcpy()
#include <string>
using std::cin;
using std::cout;
using std::string;
//以下 MyString 是為了測試 containers with moveable elements 效果.
class MyString {
public:
static size_t DCtor; //累計 default-ctor 的呼叫次數
static size_t Ctor; //累計 ctor 的呼叫次數
static size_t CCtor; //累計 copy-ctor 的呼叫次數
static size_t CAsgn; //累計 copy-asgn 的呼叫次數
static size_t MCtor; //累計 move-ctor 的呼叫次數
static size_t MAsgn; //累計 move-asgn 的呼叫次數
static size_t Dtor; //累計 dtor 的呼叫次數
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
//default ctor
MyString() : _data(NULL), _len(0) { ++DCtor; }
//ctor
MyString(const char* p) : _len(strlen(p)) {
++Ctor;
_init_data(p);
}
// copy ctor
MyString(const MyString& str) : _len(str._len) {
++CCtor;
_init_data(str._data); //COPY
}
//move ctor, with "noexcept", 和copy ctor的区别在于参数有两个&,只是拷贝指针
MyString(MyString&& str) noexcept : _data(str._data), _len(str._len) {
++MCtor;
str._len = 0;
str._data = NULL; //避免 delete (in dtor)
}
//copy assignment
MyString& operator=(const MyString& str) {
++CAsgn;
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_init_data(str._data); //COPY!
}
else {
// Self Assignment, Nothing to do.
}
return *this;
}
//move assignment
MyString& operator=(MyString&& str) noexcept {
++MAsgn;
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_data = str._data; //MOVE!
str._len = 0;
str._data = NULL; //避免 deleted in dtor
}
return *this;
}
//dtor
virtual ~MyString() {
++Dtor;
if (_data) { //
delete _data;
}
}
bool
operator<(const MyString& rhs) const //為了讓 set 比較大小
{
return std::string(this->_data) < std::string(rhs._data); //借用事實:string 已能比較大小.
}
bool
operator==(const MyString& rhs) const //為了讓 set 判斷相等.
{
return std::string(this->_data) == std::string(rhs._data); //借用事實:string 已能判斷相等.
}
char* get() const { return _data; }
};
//在class之外给static数据定义
size_t MyString::DCtor=0;
size_t MyString::Ctor=0;
size_t MyString::CCtor=0;
size_t MyString::CAsgn=0;
size_t MyString::MCtor=0;
size_t MyString::MAsgn=0;
size_t MyString::Dtor=0;
namespace std //必須放在 std 內
{
template<>
struct hash<MyString> //這是為了 unordered containers
{
size_t
operator()(const MyString& s) const noexcept
{ return hash<string>()(string(s.get())); }
//借用現有的 hash<string> (in ...\include\c++\bits\basic_string.h)
};
}
class MyStrNoMove {
public:
static size_t DCtor; //累計 default-ctor 的呼叫次數
static size_t Ctor; //累計 ctor 的呼叫次數
static size_t CCtor; //累計 copy-ctor 的呼叫次數
static size_t CAsgn; //累計 copy-asgn 的呼叫次數
static size_t MCtor; //累計 move-ctor 的呼叫次數
static size_t MAsgn; //累計 move-asgn 的呼叫次數
static size_t Dtor; //累計 dtor 的呼叫次數
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
//default ctor
MyStrNoMove() : _data(NULL), _len(0) { ++DCtor; _init_data("jjhou"); }
//ctor
MyStrNoMove(const char* p) : _len(strlen(p)) {
++Ctor; _init_data(p);
}
// copy ctor
MyStrNoMove(const MyStrNoMove& str) : _len(str._len) {
++CCtor;
_init_data(str._data); //COPY
}
//copy assignment
MyStrNoMove& operator=(const MyStrNoMove& str) {
++CAsgn;
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_init_data(str._data); //COPY!
}
else {
// Self Assignment, Nothing to do.
}
return *this;
}
//dtor
virtual ~MyStrNoMove() {
++Dtor;
if (_data) {
delete _data;
}
}
bool
operator<(const MyStrNoMove& rhs) const //為了讓 set 比較大小
{
return string(this->_data) < string(rhs._data); //借用事實:string 已能比較大小.
}
bool
operator==(const MyStrNoMove& rhs) const //為了讓 set 判斷相等.
{
return string(this->_data) == string(rhs._data); //借用事實:string 已能判斷相等.
}
char* get() const { return _data; }
};
size_t MyStrNoMove::DCtor=0;
size_t MyStrNoMove::Ctor=0;
size_t MyStrNoMove::CCtor=0;
size_t MyStrNoMove::CAsgn=0;
size_t MyStrNoMove::MCtor=0;
size_t MyStrNoMove::MAsgn=0;
size_t MyStrNoMove::Dtor=0;
namespace std //必須放在 std 內
{
template<>
struct hash<MyStrNoMove> //這是為了 unordered containers
{
size_t
operator()(const MyStrNoMove& s) const noexcept
{ return hash<string>()(string(s.get())); }
//借用現有的 hash<string> (in ...\4.9.2\include\c++\bits\basic_string.h)
};
}
#include <ctime> //clock_t, clock()
template<typename M, typename NM>
void test_moveable(M c1, NM c2, long& value)
{
char buf[10];
//測試 move
cout << "\n\ntest, with moveable elements" << endl;
typedef typename iterator_traits<typename M::iterator>::value_type V1type;
clock_t timeStart = clock();
for(long i=0; i< value; ++i)
{
snprintf(buf, 10, "%d", rand());
auto ite = c1.end();
c1.insert(ite, V1type(buf)); //所有容器都提供insert, V1type(buf)是个临时对象,右值,所以编译器会调用其move版本
}
cout << "construction, milli-seconds : " << (clock()-timeStart) << endl;
cout << "size()= " << c1.size() << endl;
output_static_data(*(c1.begin()));
timeStart = clock();
M c11(c1); //将整个容器拷贝到c11,深拷贝。 c1不是临时对象
cout << "copy, milli-seconds : " << (clock()-timeStart) << endl;
timeStart = clock();
M c12(std::move(c1)); //move copy,浅拷贝。
cout << "move copy, milli-seconds : " << (clock()-timeStart) << endl;
timeStart = clock();
c11.swap(c12); //交换
cout << "swap, milli-seconds : " << (clock()-timeStart) << endl;
//測試 non-moveable
cout << "\n\ntest, with non-moveable elements" << endl;
typedef typename iterator_traits<typename NM::iterator>::value_type V2type;
timeStart = clock();
for(long i=0; i< value; ++i)
{
snprintf(buf, 10, "%d", rand());
auto ite = c2.end();
c2.insert(ite, V2type(buf));
}
cout << "construction, milli-seconds : " << (clock()-timeStart) << endl;
cout << "size()= " << c2.size() << endl;
output_static_data(*(c2.begin()));
timeStart = clock();
NM c21(c2);
cout << "copy, milli-seconds : " << (clock()-timeStart) << endl;
timeStart = clock();
NM c22(std::move(c2));
cout << "move copy, milli-seconds : " << (clock()-timeStart) << endl;
timeStart = clock();
c21.swap(c22);
cout << "swap, milli-seconds : " << (clock()-timeStart) << endl;
}
move
之后原来的东西不再使用,才可以选择使用move copy,否则可能会带来隐患:两个指针指向同一块内存。
vector的copy ctor
说明:
M c11(c1)
这是vector的深拷贝,即如上图的流程;
说明:
M c12(std::move(c1));
这是调用的vector的move ctor,只是将三个指针进行交换,速度很快;
std::string 是否 moveable?
说明:
- std::string 的实际类型是
basic_string
; basic_string
中有构造函数,拷贝构造函数,move assign和move constructor,所以std::string是moveable的,可以安心使用;