侯捷 C++11/14 笔记
原文地址:侯捷 C++11/14 笔记
Variadic Template
概述
可变参数模板。
-
谈的是模板Templates:
- 函数模板
- 类模板
-
变化的是模板参数:
- 参数个数:利用参数个数逐一递减的特性,实现递归函数的调用,使用函数模板完成。
- 参数类型:利用参数个数逐一递减以致参数类型也逐一递减的特性,实现递归继承或递归复合,以类模板完成。
void print()
{
}
template <typename T, typename... Types> //这里的...是关键字的一部分:模板参数包
void print(const T& firstArg, const Types&... args) //这里的...要写在自定义类型Types后面:函数参数类型包
{
cout << firstArg << endl;
print(args...); //这里的...要写在变量args后面:函数参数包
}
-
注意三种不同的
...
的应用环境,这些都是语法规则,所以记住即可。 -
还要注意的是,在可变模板参数内部可以使用
sizeof...(args)
得到实参的个数。 -
如果同时定义了:
template <typename... Types>
void print(const Types&... args)
{/*......*/}
该函数重载了void print()
,void print(const T& firstArg, const Types&... args)
是其特化版本,编译器会优先调用特化版本。
应用
-
实现函数的 递归调用
举了一个unordered容器中hash函数的计算例子:万用的哈希函数,函数入口
return hash_val(c.fname, c.lname, c.no)
;
class CustomerHash{
public:
std::size_t operator() (const Customer& c) const {
return hash_val(c.fname, c.lname, c.no);
// 2-1-1-...-1-3
}
};
template <typename T, typename... Types> // 1
inline void hash_val(size_t& seed, const T& val, const Types&... args){
hash_combine(seed, val);
hash_val(seed, args);
}
template <typename... Types> // 2
inline size_t hash_val(const Types&... args){
size_t seed = 0;
hash_val(seed, args...);
return seed;
}
template <typename T> // 3
inline void hash_val(size_t& seed, const T& val){
hash_conbine(seed, val);
}
- 实现递归继承
template <typename... Values> class tuple;
template <> class tuple<> {};
template <typename Head, typename... Tail>
class tuple<Head, Tail...>
: private tuple<Tail...> //注意这里的私有继承
{
typedef tuple<Tail...> inherited;
public:
tuple() {}
tuple(Head v, Tail... vtail)
:m_head(v), inherited(vtail...) {}
Head head() { return m_head; }
inherited& tail() { return *this; } //这里涉及派生类到基类的类型转换
protected:
Head m_head;
};
模板表达式中的空格
C++11可以去掉模块表达式前面的空格。
nullptr
标准库允许使用nullptr取代0或者NULL来对指针赋值。
- nullptr 是个新关键字
- nullptr 可以被自动转换为各种 pointer 类型,但不会被转换为任何整数类型
- nullptr的类型为std::nullptr_t,定义于 头文件中
void f(int);
void f(void *);
f(0); // 调用 f(int).
f(NULL); // 如果定义NULL为0,则调用 f(int),否则具有二义性
f(nullptr); // 调用 f(void *).
auto
- C++11 auto可以进行自动类型推导。
- C语言默认的局部变量是auto类型的
- C++11 auto可以进行自动类型推导
- 使用auto的场景:类型太长或者类型太复杂
一致性初始化(uniform initialization)
C++11之前初始化时存在多个版本 {},(),=
。让使用者使用时比较混乱,C++11提供一种万用的初始化方法,就是使用大括号{}
。
原理解析:当编译器看到大括号包起来的东西{t1,t2...tn}
时,会生成一个initializer_list<T>
(initializer_list
关联至一个array<T,n>
)。调用函数(例如构造函数ctor
)时该array
内的元素可被编译器分解逐一传给函数;元素逐一分解传递给函数进行初始化。
但是如果调用函数自身提供了initializer_list<T>
参数类型的构造函数时,则不会分解而是直接传过去。直接整包传入进行初始化。所有的容器都可以接受这样的参数。
Initializer_list
int i; // 未初始化
int j{}; // j = 0
int* p; // 未初始化
int* q{}; // q = nullptr
-
initializer_list<T>
使用举例:initializer_list<T>
是一个class
(类模板),这个必须类型要一致,跟模板不定的参数类型相比,模板不定的参数类型可以都不一样。initializer_list<T>
类似于容器的使用方法
-
initializer_list
源码剖析:initializer_list<T>
背后有array
数组支撑,initializer_list
关联一个array<T,n>
initializer_list<T>
包含一个指向array
的指针,它的拷贝只是一个浅拷贝,比较危险,两个指针指向同一个内存。
-
initializer_list在STL中的使用:
- 所有容器都接受指定任意数量的值用于构造或赋值或者
insert()
或assign()
。 - 算法
max()
和min()
也接受任意参数。
- 所有容器都接受指定任意数量的值用于构造或赋值或者
explict
explicit
关键字一直存在,只能作用在构造函数中,目的是阻止编译器进行不应该允许的构造函数进行隐式转换。声明为explicit
的构造函数不能进行隐式转换,只能允许使用者明确调用构造函数。
C++11之前,只有non-explicit one argument
的构造函数才能进行隐式转换,2.0之后支持more than one argument
的构造函数的隐式转换。
基于范围的for循环
for (decl : coll) {
statement
}
// 例子
vector<double> vec;
//...
for(auto elem: vec) {...}; // 赋值,无法改变容器的内容
for(auto& elem: vec) {...}; // 引用
基于范围的for
循环对于explicit
类型申明的转换是不可以的。
= default, = delete
在 C++ 中,如果自定义了 big-five 函数,编译器就不会再生成默认的相关函数,但是如果我们在后边加上= default
关键字,就可以重新获得并使用编译器为我们生成的默认函数(显式缺省:告知编译器即使自己定义了也要生成函数默认的缺省版本);
=delete
关键字相对于上面来说则是相反的,=delete
表示不要这个函数,就是说这个函数已经删除了不能用了,一旦别人使用就会报错(显式删除:告知编译器不生成函数默认的缺省版本),引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。
补充:
1、编译器提供的默认函数:
C++中,当我们设计与编写一个类时,若不显著申明,则类会默认为我们提供如下几个函数:
- 构造函数(
A()
) - 析构函数(
~A()
) - 拷贝构造函数(
A(A&)
) - 拷贝赋值函数(
A& operator=(A&)
) - 移动构造函数(
A(A&&)
) - 移动赋值函数(
A& operator=(A&&)
)
注意:拷贝函数如果涉及指针就要区分浅拷贝(指针只占4字节,浅拷贝只把指针所占的那4个字节拷贝过去)和深拷贝(不仅要拷贝指针所占的字节,还要把指针所指的东西也要拷贝过去);
默认提供全局的默认操作符函数:
operator
operator &
operator &&
operator *
operator->
operator->*
operator new
operator delete
2、何时需要自定义big-three(构造函数、拷贝构造、拷贝赋值)/big-five(新增移动构造函数、移动赋值函数)?
如果类中带有pointer member
(指针成员),那我们就可以断定必须要给出 big-three ;
如果不带,绝大多与情况下就不必给出 big-three 。
3、default
、delete
关键字使用示例
在c++中,如果你自定义了big-five函数,编译器就不会再为你生成默认的相关函数,但是如果我们在后边加上= default
关键字,就可以重新获得并使用编译器为我们生成的默认函数(显式缺省:告知编译器即使自己定义了也要生成函数默认的缺省版本);
=delete
关键字相对于上面来说则是相反的,=delete
表示不要这个函数,就是说这个函数已经删除了不能用了,一旦别人使用就会报错(显式删除:告知编译器不生成函数默认的缺省版本),引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。
Alias Template
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>
//使用
Vec<int> coll;
Alias Template 无法特化。
应用实例(引出模板模板参数)
考虑这样一种需求,假设我们需要实现一个函数test_moveable(容器对象,类型对象)
,从而能实现传入任意的容器和类型,都能将其组合为一个新的东西:容器<类型>,这样的话我们的函数应该怎么设计呢?
(1)解法一:函数模板(无法实现)
template <typename Container, typename T>
void test_moveable(Container cntr, T elem)
{
Container<T> c; //[Error] 'Container' is not a template
for(long i=0; i<SIZE; ++i)
c.insert(c.end(), T());
output_static_data(T());
Container<T> c1(c);
Container<T> c2(std::move(c));
c1.swap(c2);
}
(2)解法二:函数模板+iterator+traits(可以实现)
template<typename Container>
void test_moveable(Container c)
{
typedef typename iterator_traits<typename Container::iterator>::value_type Valtype;
for(long i=0; i<SIZE; ++i)
c.insert(c.end(), Valtype());
output_static_data(*(c.begin()));
Container<T> c1(c);
Container<T> c2(std::move(c));
c1.swap(c2);
}
这样做是可以达到效果的,但是却改变了函数签名,使用的时候我们需要这样调用:test_moveable(list<int>())
,和我们开始设计的是不一样的。那么,有没有 template 语法能够在模板接受一个 template 参数 Container 时,当 Container 本身又是一个 class template ,能取出 Container 的template 参数?例如收到一个vector<string>
,能够取出其元素类型string
?那么这就引出了模板模板参数的概念。也就是下面的解法三。
(3)解法三:模板模板参数 + alias template(可以实现)
template <typename T,
template <typename T> // 模板模板参数中的T可以不写,默认就是前面的T
class Container
>
class XCls
{
private:
Container<T> c;
public:
XCLs()
{
for(long i=0; i<SIZE; ++i)
c.insert(c.end(), T());
output_static_data(T());
Container<T> c1(c);
Container<T> c2(std::move(c));
c1.swap(c2);
}
};
// 使用时会报错
XCls<MyString, vector> c1; //[Error] vector的实际类型和模板中的Container<T>类型不匹配
这是因为 vector
其实有两个模板参数,虽然第二个有默认值,我们平时也可以像vector<int>
这样用。但是在模板中直接这样写类型是不匹配的( Container
只有一个模板参数 )。所以这里就用到了我们一开始提到的模板别名,只要传入的是vector
的模板别名就可以了,如下所示:
//不得在function body之内声明
template<typename T>
using Vec = vector<T, allocator<T>>;
XCls<MyString, Vec> c1;
Type Alias
类型别名类似于typedef。
using func = void(*)(int, int);
//相当于
typedef void (*func)(int, int);
using
的用法:
- 打开命令空间或者命令空间的成员
- 类似第一种,打开类的成员
- 类型别名和模板别名(C++ 11开始支持)
using std::cin; //1
using _Base::_M_alloacte; //2
using func = void(*)(int, int); //3
noexpect
保证该函数不会丢出异常,可以在后面加上条件,也就是说在某种条件满足情况下,不会抛出异常。
void foo() noexpect;
void foo() noexpect(true);
一般异常处理流程:当程序发生异常时会将异常信息上报返回给调用者,如果有异常处理则处理,如果该调用者没有处理异常则会接着上报上一层,若到了最上层都没有处理,就会调用std::terminate()->std::abort()
,然后终止程序。
{% note warning %}
移动构造函数和移动赋值函数。如果构造函数没有noexcept
,vector
将不敢使用它。
{% endnote %}
override
override
用于明确要重写父类的虚函数上,相当于告诉编译器这个函数就是要重写父类虚函数这样一个意图,让编译器帮忙检查,而没有这个关键字,编译器是不会帮你检查的。
final
final
新增两种功能:
- 禁止基类被继承
- 禁止虚函数被重写
decltype
decltype 定义
引入新关键字decltype
可以让编译器找出表达式的类型,为了区别typeof
,以下做一个概念区分:
typeof
是一个一元运算,放在一个运算数之前,运算数可以是任意类型,非常依赖平台,已过时,由decltype
代替;理解为:我们根据typeof()
括号里面的变量,自动识别变量类型并返回该类型;typedef
:定义一种类型的别名,而不只是简单的宏替换;define
:简单的宏替换;typeid()
返回变量类型的字符串,用于print
变量类型。
decltype 用法
- 用来声明函数的返回值类型,一种新的指定函数返回值类型的方式;
template<typename T1, typename T2>
auto Add(T1 x, T2 y) -> decltype(x + y);
- 模板之间的应用
-
用来求 lambda 表达式的类型
lambda 是匿名的函数对象或仿函数,每一个都是独一无二的;如果需要声明一个这种对象的话,需要用模板或者 auto ;如果需要他的 type ,可以使用 decltype ;lambda 没有默认构造函数和析构函数。
Lambdas
lambda 语法以及调用方式
定义: lambda 是一组功能的组合定义, lambda 可以定义为内联函数,可以被当做一个参数或者一个对象,类似于仿函数。
最简单的形式:
[] {
statements
};
auto l = [] {
statements
};
l();
完整形式:
含义 | |
---|---|
[] | lambda 导入器,取用外部变量 |
() | 类似函数参数 |
mutable | []中的导入数据是否可变 |
throwSpec | 抛出异常 |
retType | 类似函数返回值 |
{} | 类似函数体 |