侯捷 C++11/14 笔记

Variadic Template

概述

可变参数模板。

  1. 谈的是模板Templates:

    • 函数模板
    • 类模板
  2. 变化的是模板参数:

    • 参数个数:利用参数个数逐一递减的特性,实现递归函数的调用,使用函数模板完成。
    • 参数类型:利用参数个数逐一递减以致参数类型也逐一递减的特性,实现递归继承或递归复合,以类模板完成。
1
2
3
4
5
6
7
8
9
10
void print()
{
}

template <typename T, typename... Types> //这里的...是关键字的一部分:模板参数包
void print(const T& firstArg, const Types&... args) //这里的...要写在自定义类型Types后面:函数参数类型包
{
cout << firstArg << endl;
print(args...); //这里的...要写在变量args后面:函数参数包
}
  1. 注意三种不同的...的应用环境,这些都是语法规则,所以记住即可。

  2. 还要注意的是,在可变模板参数内部可以使用sizeof...(args)得到实参的个数。

  3. 如果同时定义了:

1
2
3
template <typename... Types>
void print(const Types&... args)
{/*......*/}

该函数重载了void print()void print(const T& firstArg, const Types&... args) 是其特化版本,编译器会优先调用特化版本。

应用

  1. 实现函数的 递归调用

    举了一个unordered容器中hash函数的计算例子:万用的哈希函数,函数入口return hash_val(c.fname, c.lname, c.no);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

递归调用

  1. 实现递归继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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,定义于头文件中
1
2
3
4
5
6
void f(int);
void f(void *);

f(0); // 调用 f(int).
f(NULL); // 如果定义NULL为0,则调用 f(int),否则具有二义性
f(nullptr); // 调用 f(void *).

auto

  1. C++11 auto可以进行自动类型推导。
    • C语言默认的局部变量是auto类型的
    • C++11 auto可以进行自动类型推导
  2. 使用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

1
2
3
4
int i;      // 未初始化
int j{}; // j = 0
int* p; // 未初始化
int* q{}; // q = nullptr
  1. initializer_list<T>使用举例:

    • initializer_list<T>是一个class(类模板),这个必须类型要一致,跟模板不定的参数类型相比,模板不定的参数类型可以都不一样。
    • initializer_list<T>类似于容器的使用方法
  2. initializer_list源码剖析:

    • initializer_list<T>背后有array数组支撑,initializer_list关联一个array<T,n>
    • initializer_list<T>包含一个指向array的指针,它的拷贝只是一个浅拷贝,比较危险,两个指针指向同一个内存。
  3. initializer_list在STL中的使用:

    • 所有容器都接受指定任意数量的值用于构造或赋值或者insert()assign()
    • 算法max()min()也接受任意参数。

explict

explicit关键字一直存在,只能作用在构造函数中,目的是阻止编译器进行不应该允许的构造函数进行隐式转换。声明为explicit的构造函数不能进行隐式转换,只能允许使用者明确调用构造函数。

C++11之前,只有non-explicit one argument的构造函数才能进行隐式转换,2.0之后支持more than one argument的构造函数的隐式转换。

基于范围的for循环

1
2
3
4
5
6
7
8
9
10
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++中,当我们设计与编写一个类时,若不显著申明,则类会默认为我们提供如下几个函数:

  1. 构造函数(A())
  2. 析构函数(~A()
  3. 拷贝构造函数(A(A&))
  4. 拷贝赋值函数(A& operator=(A&)
  5. 移动构造函数(A(A&&)
  6. 移动赋值函数(A& operator=(A&&)

注意:拷贝函数如果涉及指针就要区分浅拷贝(指针只占4字节,浅拷贝只把指针所占的那4个字节拷贝过去)和深拷贝(不仅要拷贝指针所占的字节,还要把指针所指的东西也要拷贝过去);

默认提供全局的默认操作符函数:

  1. operator
  2. operator &
  3. operator &&
  4. operator *
  5. operator->
  6. operator->*
  7. operator new
  8. operator delete

2、何时需要自定义big-three(构造函数、拷贝构造、拷贝赋值)/big-five(新增移动构造函数、移动赋值函数)?

如果类中带有pointer member(指针成员),那我们就可以断定必须要给出 big-three ;
如果不带,绝大多与情况下就不必给出 big-three 。

3、defaultdelete关键字使用示例

=default, =delete

在c++中,如果你自定义了big-five函数,编译器就不会再为你生成默认的相关函数,但是如果我们在后边加上= default关键字,就可以重新获得并使用编译器为我们生成的默认函数(显式缺省:告知编译器即使自己定义了也要生成函数默认的缺省版本);

=delete关键字相对于上面来说则是相反的,=delete表示不要这个函数,就是说这个函数已经删除了不能用了,一旦别人使用就会报错(显式删除:告知编译器不生成函数默认的缺省版本),引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。

image-20220802201319128

Alias Template

1
2
3
4
5
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>

//使用
Vec<int> coll;

Alias Template 无法特化。

应用实例(引出模板模板参数)

考虑这样一种需求,假设我们需要实现一个函数test_moveable(容器对象,类型对象),从而能实现传入任意的容器和类型,都能将其组合为一个新的东西:容器<类型>,这样的话我们的函数应该怎么设计呢?

(1)解法一:函数模板(无法实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
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(可以实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
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(可以实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的模板别名就可以了,如下所示:

1
2
3
4
5
//不得在function body之内声明
template<typename T>
using Vec = vector<T, allocator<T>>;

XCls<MyString, Vec> c1;

Type Alias

类型别名类似于typedef。

1
2
3
using func = void(*)(int, int);
//相当于
typedef void (*func)(int, int);

using 的用法:

  1. 打开命令空间或者命令空间的成员
  2. 类似第一种,打开类的成员
  3. 类型别名和模板别名(C++ 11开始支持)
1
2
3
using std::cin;                 //1
using _Base::_M_alloacte; //2
using func = void(*)(int, int); //3

noexpect

保证该函数不会丢出异常,可以在后面加上条件,也就是说在某种条件满足情况下,不会抛出异常。

1
2
void foo() noexpect;
void foo() noexpect(true);

一般异常处理流程:当程序发生异常时会将异常信息上报返回给调用者,如果有异常处理则处理,如果该调用者没有处理异常则会接着上报上一层,若到了最上层都没有处理,就会调用std::terminate()->std::abort(),然后终止程序。

移动构造函数和移动赋值函数。如果构造函数没有noexceptvector将不敢使用它。

override

override用于明确要重写父类的虚函数上,相当于告诉编译器这个函数就是要重写父类虚函数这样一个意图,让编译器帮忙检查,而没有这个关键字,编译器是不会帮你检查的。

override

final

final新增两种功能:

  1. 禁止基类被继承
  2. 禁止虚函数被重写

decltype

decltype 定义

引入新关键字decltype可以让编译器找出表达式的类型,为了区别typeof,以下做一个概念区分:

  • typeof是一个一元运算,放在一个运算数之前,运算数可以是任意类型,非常依赖平台,已过时,由decltype代替;理解为:我们根据typeof()括号里面的变量,自动识别变量类型并返回该类型;
  • typedef:定义一种类型的别名,而不只是简单的宏替换;
  • define:简单的宏替换;
  • typeid() 返回变量类型的字符串,用于print变量类型。

decltype 用法

  1. 用来声明函数的返回值类型,一种新的指定函数返回值类型的方式;
1
2
template<typename T1, typename T2>
auto Add(T1 x, T2 y) -> decltype(x + y);
  1. 模板之间的应用

模板之间的应用

  1. 用来求 lambda 表达式的类型

    lambda 是匿名的函数对象或仿函数,每一个都是独一无二的;如果需要声明一个这种对象的话,需要用模板或者 auto ;如果需要他的 type ,可以使用 decltype ;lambda 没有默认构造函数和析构函数。

lambda表达式

Lambdas

lambda 语法以及调用方式

定义: lambda 是一组功能的组合定义, lambda 可以定义为内联函数,可以被当做一个参数或者一个对象,类似于仿函数。

最简单的形式:

1
2
3
4
5
6
7
8
9
[] {
statements
};

auto l = [] {
statements
};

l();

完整形式:
lambda表达式

含义
[]lambda 导入器,取用外部变量
()类似函数参数
mutable[]中的导入数据是否可变
throwSpec抛出异常
retType类似函数返回值
{}类似函数体

image-20220802201742502

image-20220802201822504


侯捷 C++11/14 笔记
https://ww1820.github.io/posts/bea73fec/
作者
AWei
发布于
2022年7月21日
更新于
2022年8月2日
许可协议