C++11新特性选讲 语言部分 侯捷
本课程分为两个部分:语言的部分和标准库的部分。只谈新特性,并且是选讲。
本文为语言部分笔记。
- 语言
- Variadic Templates
- move semantics
- auto
- Range-based for loop
- Initializer list
- Lambdas
- …
- 标准库
- type_traits
- unodered containers
- forward_list
- array
- tuple
- concurrency
- RegEx
- …
关于头文件
C++11的新特性包含语言和标准库两部分,后者以头文件 header files 的形式呈现。
关于C++的头文件,有以下几点:
- C++标准库的头文件均不带 .h,如
#include <iostream>
- 在C++中,旧式的C的头文件(带有 .h)依然可用,如
#include <stdio.h>
- 建议在C++中使用,新式的C头文件,与旧式的关系:xxx.h -> cxxx,如
#include <cstdio>
Variatic Templates
函数变参模板
假如我想设计一个函数 print,它能够接收任意数量的参数,并且这个参数的类型也是任意的,就可以利用 Variatic Templates 来递归地实现:
#include <iostream>
void print() {} // 1
template<typename T, typename... Types> // 2
void print(const T& firstArg, const Types&... args) { // 3
std::cout << firstArg << std::endl;
print(args...); // 4
}
int main() {
print("dfafda", 's', 123);
}
注意这里的 ...
可不是我们口语中的省略号,而是实实在在的C++11新语法的一部分,可以将它理解为一个 pack (包),具体是什么 “包”,则取决于它出现在哪里。在本例中,...
共出现了三次:
- 用于 template parameters,就是 template parameters pack,”模板参数包“,如2处
- 用于 function parameters types,就是 function parameters types pack,”函数参数类型包“,如3处
- 用于 function parameters,就是 function parameters pack,“函数参数包”,如4处
在变参模板中,如果我们想要知道可变参数的个数,可通过:sizeof...(args)
。
注意除了2处, 我们在1处定义了一个空参数列表的 print 函数的重载版本,它在 print 函数地参数列表中的参数被递归地打印完之后被调用,其实就是相当于我们 print 函数的递归退出的条件。
思考:以下这个 print 函数的重载版本能够与上面的 print 函数并存吗,如果可以,谁比较泛化,谁比较特化呢?(我们知道,两个版本均可的情况下,较为特化的版本会被优先调用)
template <typename... Types>
void print(const Types&... args) {
/* --- */
}
变参模板的花式应用:
- 万能的hash function:多种函数重载 + 递归 + 函数变参模板 —>花式调用
- tuple:类变参模板 + 继承
类变参模板
一些”小“的新特性
Spaces in Template Expressions
在C++11之前,如果有模板嵌套,右侧的两个尖括号不能靠在一起,中间须有空格,否则编译器会认为那是个右移运算符,在C++11之后编译器变聪明了,不再需要这个空格。
vector<list<int> >; // OK in each C++ version
vector<list<int>>; // OK since C++ 11
nullptr and std::nullptr_t
在C++11之后,我们可以使用 nullptr
(而非之前的 NULL
或者 0)来表示空指针。注意 NULL
就是一个宏,其值为0,而 nullptr
确实是个指针,其类型为 std::nullptr_t
。下面的例子可以验证:
#include <iostream>
void f(int) {
std::cout << "call f(int)" << std::endl;
}
void f(void*) {
std::cout << "call f(void*)" << std::endl;
}
int main() {
f(0); // calls f(int)
f(NULL); // calls f(int) if NULL is 0; ambiguous otherwise
f(nullptr); // calls f(void*)
}
auto
自动类型推导 auto:在C++11之后,可以用 auto
来定义变量的类型,编译器会自动进行类型推导。
auto i = 42; // i是int类型
double f();
auto d = f(); // d是double类型
注意:不建议在任何时候都使用 auto ,而是推荐在这个变量的类型名称实在是很长或者很复杂,实在是懒得打那么多字时使用,但是我们要知道变量应该是什么类型,如:
vector<string> v;
auto pos = v.begin(); // 过长
auto f = [](int x) -> bool { // 过于复杂
// ...
}
程序员要做到对自己的变量类型心中有数。
Uniform Initialization
在C++11之前,许多程序员会疑惑,一个变量或者对象的初始化可能会发生于小括号,大括号,赋值运算符。如:
vector<int> vec(3, 5);
vector<int> vec {1, 2 ,3};
int a = 1;
C++11引入一致初始化,使用大括号,在变量后面直接跟大括号,大括号中可以有任意多个元素个数,设置初值,进行初始化,如:
int values[] {1, 2, 3};
vector<int> v {2, 3, 4};
complex<double> {4.0, 3.0};
当然之前的语法也是可用的。
实际上,编译器看到 {}
就会作出一个 initializer_list<T>
,它关联至一个 array<T, n>
。调用函数(如ctor)时该 array 的元素被编译器分解逐一传给函数。需要注意的是:若某个函数参数就是个 initializer_list<T>
,调用者不能传递数个 T 参数然后以为它们会被自动转换为一个 intializer_list<T>
传入,即需要自己手动将数个参数转换为 initializer_list<T>
再进行传值。
比如:
vector<string> cities {"Berlin", "New York", "London"};
这形成一个 initializer_list<string>
,背后有个 array<string, 3>
。调用 vector<string>
的 ctors(构造函数)中的接收 initialize_list<string>
的版本,标准库中所有容器都有接收这个 initializer_list<T>
的构造函数。
但是对于我们自己的类,可能没有接收 intializer_list<T>
这种参数的构造函数,此时这个初始化列表逐一分解拆成一个一个的参数传给函数,再去找与多个单个参数相匹配的构造函数。
initializer_list
初始化列表是支持上面提到的大括号形式的一致性初始化的背后方法。
为了支持用户自定义的类的 initializer_list。C++11提供了一个类模板:std::initializer_list<T>
。他可以用用于使用一包参数值来进行初始化,或者用来其他你想要处理一包参数的地方。如使用initalizer_list传参:
#include <iostream>
void print(std::initializer_list<int> vals) {
for (auto ite = vals.begin(); ite!=vals.end(); ++ite) {
std::cout << *ite << "\n";
}
}
int main() {
print( {1,2,3,4} ); // 使用initalizer_list传参
}
-
即
{}
即可形成一个 initializer_list -
不同于前面的 variadic template,这里的 initializer_list 需要的是固定类型 T 的任意多个参数。也可以看做是一种容器。
-
initializer_list背后由array构建。
-
intializer_list如果被拷贝,会是浅拷贝(引用语义)
在C++11之后的标准库中,initializers_list
有许多应用,最常见的肯定是上面提到过的各个容器的构造函数中可以使用其作为参数。另外,在一些算法中也有应用,比如 min/max 函数,在C++11之前,它们只能支持两个元素的比较:
std::min(1, 2);
在C++11之后,借助 initializer_list 它可以支持多个元素的比较:
std::min( {1, 2, 3, 4} );
range-based for loop
在C++11之后
for (decl : coll) {
statement;
}
如:
std::vector<int> vec = {1, 2, 3, 4};
for (int i : vec) {
std::cout << i << std::endl;
}
也可以用引用:
std::vector<double> vec;
for (auto& elem : vec) {
elem *= 3; // 因为是引用,所以会改变原vector
}
类似python的for loop:
for i in range(10):
print(i)
实际上,这种for loop的背后实现就是将该容器的迭代器取出来,并遍历一遍,并将遍历过程中的每个元素赋值到左侧声明出来的变量。
这种for loop赋值时可能会做隐式类型转换。
=default, =delete
如果你自行定义了一个 ctor,那么编译器就不会再给你一个 default ctor;但是如果你强制加上 =default
(可以空格),就可以重新获得并使用默认的 default ctor。而如果加上 =delete
,则是禁用该成员函数的使用。
class Zoo {
public:
Zoo(int i1, int i2) : d1(i1), d2(i2) {} // 构造函数
Zoo(const Zoo&) = delete; // 拷贝构造
Zoo(Zoo&&) = default; // 移动构造
Zoo& operator=(const Zoo&) = default; // 拷贝赋值
Zoo& operator=(const Zoo&&) = delete; // 移动赋值
virtual ~Zoo() {} // 析构函数
private:
int d1, d2;
}
=default
每当我们声明一个有参构造函数时,编译器就不会创建默认构造函数。在这种情况下,我们可以使用 =default
说明符来创建默认的构造函数。以下代码演示了如何创建:
// use of defaulted functions
#include <iostream>
using namespace std;
class A {
public:
// A user-defined
A(int x){
cout << "This is a parameterized constructor";
}
// Using the default specifier to instruct
// the compiler to create the default implementation of the constructor.
A() = default;
};
int main(){
A a; //call A()
A x(1); //call A(int x)
cout<<endl;
return 0;
}
=delete
在C ++ 11之前,操作符delete 只有一个目的,即释放已动态分配的内存。而C ++ 11标准引入了此操作符的另一种用法,即:禁用成员函数的使用。这是通过附加 = delete
来完成的; 说明符到该函数声明的结尾。
使用 = delete
说明符禁用其使用的任何成员函数称为explicitly deleted函数。
虽然不限于它们,但这通常是针对隐式函数。以下示例展示了此功能派上用场的一些任务:
禁用拷贝构造函数
// copy-constructor using delete operator
#include <iostream>
using namespace std;
class A {
public:
A(int x): m(x) { }
// Delete the copy constructor
A(const A&) = delete;
// Delete the copy assignment operator
A& operator=(const A&) = delete;
int m;
};
int main() {
A a1(1), a2(2), a3(3);
// Error, the usage of the copy assignment operator is disabled
a1 = a2;
// Error, the usage of the copy constructor is disabled
a3 = A(a2);
return 0;
}
禁用不需要的类型转换
// type conversion using delete operator
#include <iostream>
using namespace std;
class A {
public:
A(int) {}
// Declare the conversion constructor as a deleted function. Without this step,
// even though A(double) isn't defined, the A(int) would accept any double value
// for it's argumentand convert it to an int
A(double) = delete;
};
int main() {
A A1(1);
// Error, conversion from double to class A is disabled.
A A2(100.1);
return 0;
}
请注意,删除的函数是隐式内联的,这一点非常重要。删除的函数定义必须是函数的第一个声明。换句话说,以下方法是将函数声明为已删除的正确方法:
class C {
public:
C(C& a) = delete;
};
但是以下尝试声明删除函数的方法会产生错误:
// incorrect syntax of declaring a member function as deleted
class C {
public:
C();
};
// Error, the deleted definition of function C must be the first declaration of the function.
C::C() = delete;
最后,明确删除函数有什么好处?
删除特殊成员函数提供了一种更简洁的方法来防止编译器生成我们不想要的特殊成员函数。(如“禁用拷贝构造函数”示例中所示)。
删除正常成员函数或非成员函数可防止有问题的类型导致调用非预期函数(如“禁用不需要的参数转换”示例中所示)。
Big Five,指每个类的拷贝控制,即构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数、移动赋值函数、析构函数。它们默认是 public 且 inline 的。
=default
不能用于 Big Five 之外的常规函数:编译会报错,因为其他函数并没有默认的版本。=delete
可以用于任何函数身上(但好像没什么意义,不需要某个函数不写就是了,为什么要写了再=delete呢),注意类似的=0
只能用于虚函数,这样会使得该虚函数称为纯虚函数,强迫子类重写该函数。
alias template (template typedef)
带参数的别名模板。
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>;
注意这里的 using
关键字并不是 C++11 的新东西,但是 using
关键字的这种使用方法是C++11之后的新的用来做 alias template 的方法。
在经过了上面的定义之后,以下两种写法是等价的:
Vec<int> coll;
// 等价于
std::vector<int, MyAlloc<int>> coll;
如此我们可以方便地使用我们自己的分配器 MyAlloc
创建一个类型可选的 vector 对象。
注意,大家注意到这种用法和我们的宏定义和 typedef
好像有些类似,但是实际上使用 macro 宏定义或 typedef
均无法实现上面的效果。
-
若使用宏定义:
#define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;
我们知道宏定义就是机械地字符替换,所以在使用时:
Vec<int> coll; // 等价于 template<typename int> std::vector<int, MyAlloc<int>>;
完全不是我们想要的意思。
-
若使用
typedef
也不行,因为typedef
是不接收参数的。至多写成:
typedef std::vector<int, MyAlloc<int>> Vec;
这当然也不是我们想要的,没办法指定变量的类型。
注意 alias template 不能做偏特化或全特化。
type alias (similar to typedef)
using value_type = T;
// 等价于
typedef T value_type;
与上面的 alias template 类似,这里的 using
关键字的这种使用方法是C++11之后的新的用来做 type alias 的方法。
using func = void(*)(int, int);
// 等价于
typedef void (*func)(int, int);
// 使用func,作为函数指针类
void example(int, int) {}
func fn = example;
后面这个例子中的 func
被定义为一种类型,它是一个函数指针类型。
using关键字总结
-
using-directives,如
using namespace std;
-
using-declarations for namespace members,如
using std::cout;
-
using-declarations for class members,如
using _Base::_M_allocate;
-
type alias (since C++11),如:
template <typename T> using Vec = std::vector<T, MyAlloc<T>>;
-
alias template (since C++11),如
using func = void(*)(int, int);
noexcept
void foo() noexcept {
// ...
}
程序员保证 foo()
函数不会抛出异常,让别人/编译器“放心地”调用该函数。
实际上 noexcept
关键字还可以加参数,来表示在…条件下,函数不会抛出异常,上面的 void foo() noexcept ;
就等价于 void foo() noexcept(true);
, 即相当于无条件保证。
而下面:
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) {
x.swap(y);
}
则意为在 x.swap(y)
不抛出异常的条件下,本函数不会抛出异常。
补充一下,异常是这样的,如果 A 调用 B,B 调用 C,而在 C 执行的过程中出现了异常,则先看 C 有没有写明异常处理程序,如果有,则处理;如果没有,则异常传递给 B,然后看 B 有没有对应的异常处理程序,如果有,则处理;如果也没有,则继续传递给 A。即按照调用顺序一层一层地向上传递,直到有对应的异常处理程序。如果用户一直没有异常处理程序,则执行 std::terminate()
,进而执行 std::abort()
,程序退出。
override
override
关键字,标明重写,应用于虚函数身上。
考虑下面这种情况:
struct Base {
virtual void vfunc(float) { }
};
struct Derived: Base {
// virtual void vfunc(int) { }
virtual void vfunc(int) override { }
}
子类 Derived
在继承父类 Base
之后想要重写父类的 void vfunc(float)
方法,但是我们知道,要重写父类方法需要函数签名完全一致,这里可能由于疏忽大意,将参数类型写为了 int。这就导致子类的这个函数定义了一个新函数,而非是期望中的对于父类函数的重写了。而编译器肯定是不知道你其实是想重写父类方法的,因为你函数签名的不一致,就按照一个新方法来处理了。
在 C++11 之后,引入了 override
关键字,在你确实想要重写的函数之后,加上这个关键字,编译器会在你在想重写但是函数签名写错的时候提醒你,这个被标记为重写函数的函数实际上并未进行重写。
final
-
修饰类,使得该类不能被继承
struct Base final {}; struct Derived: Base {}; // Error
Base 类被
final
关键字修饰,使得其不能被继承,下面的 Derived 试图继承它,会报错。 -
修饰虚函数,使得该虚函数不能被重写
struct Base { virtual void func() final; } struct Derived : Base { void func(); // Error }
Base 类本身没有被
final
修饰,所以可以被继承。但是其虚函数func()
被final
关键字修饰,故func()
不能被重写。下面的 Derive 类试图重写它,会报错。
decltype
获取一个变量/一个对象的类型 (即 tpyeof(a)
)是非常常见的需求,但是在 C++11 之前并没有直接提供这样的关键字(仅有 typeid 等)。 decltype
可以满足这一需求,方便地获得变量 / 对象的类型。
decltype
用来定义一种类型,该类型等同于一个类型表达式的结果。如 decltype(x+y)
定义了 x+y
这个表达式的返回类型。
map<string, float> coll;
decltype(coll)::value_type elem;
在C++11之前只能:
map<string, float>::value_type elem;
这当然不能让我们在未知变量 / 对象的类型的条件下知道其类型。
decltype
的三种应用场景:
1-用来声明返回值类型
有时候,函数返回值的类型取决于参数的表达式的执行结果。然而,在C++11之前,没有 decltype
之前,以下语句是不可能成立的:
template<typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);
因为上面的返回值的类型使用了尚未引入且不再作用域内的对象。
但是在C++11之后,我们可以通过在函数的参数列表之后声明一个返回值类型来实现:
template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x+y);
这与 lambda 表达式声明返回值的语法相同:
[
.
.
.
]
(
.
.
.
)
m
u
t
a
b
l
e
o
p
t
t
h
r
o
w
S
p
e
c
o
p
t
−
>
r
e
t
T
y
p
e
o
p
t
{
.
.
.
}
[...](...)\ mutable_{opt}\ throwSpec_{opt}->retType_{opt}\{...\}
[...](...) mutableopt throwSpecopt−>retTypeopt{...}
2-元编程
元编程是对模板编程的运用。
举例:
typdef typename decltype(obj)::iterator iType;
// 类似 typedef typename T::iterator iType;
decltype(obj) anotherObj(obj);
3-传递lambda的类型
面对lambda,我们手上往往只有对象,没有类型,要获得其类型就得借助于 decltype
。
如:
auto cmp = [] (const Person& p1, const Person &p2) {
return /* 给出Person类比大小的准则 */
}
//...
std::set<Person, decltype(cmp)> coll<cmp>;
我们知道由于 set 是有序容器,所以在将自定义的类构成一个 set 的时候需要给出该类比大小的准则(谓词),通常是函数、仿函数或者 lambda 表达式。但是这里我们同样需要指定类型,这就可以用 decltype
来指定。
lambdas
C++11 引入了 lambdas ,允许定义一个单行的函数,可以用作是参数或者局部对象。Lambdas 的引入改变了C++标准库的使用方式(比如原来的一些仿函数谓词,现在可直接用)。
基本用法
最简单的 lambda 函数不接收参数,并做一些简单的事情,比如这里的打印一句话:
[] { std::cout << "Hello Lambda" << std::endl; }
我们可以直接调用它,就像调用普通函数和函数对象那样,用 ()
:
[] { std::cout << "Hello Lambda" << std::endl; }();
虽然可以这样直接低啊用,但是这样其实没什么意义,因为你想要打印直接打印就好了,没必要再绕个圈子,我们通常将 lambda 函数赋值给一个变量,这样就能像调用普通函数那样多次调用它:
auto l = [] { std::cout << "Hello Lambda" << std::endl; };
l();
l();
l();
这里 lambda 对象的类型很复杂,通常也没有必要显式地写出来,我们正好用前面介绍过的 C++11 中的 auto
来简化我们的代码。如果一定要拿到 lambda 函数对象的类型,参考上面的 decltype
的用法三。
完整形式
lambda 表达式的完整形式:
[
.
.
.
]
(
.
.
.
)
m
u
t
a
b
l
e
o
p
t
t
h
r
o
w
S
p
e
c
o
p
t
−
>
r
e
t
T
y
p
e
o
p
t
{
.
.
.
}
[...]\ (...)\ mutable_{opt}\ throwSpec_{opt}->retType_{opt}\{...\}
[...] (...) mutableopt throwSpecopt−>retTypeopt{...}
- lambda 函数除了少数几处细节(如没有默认构造函数、需要加mutable),几乎完全等同于一个对应的函数对象
[]
称为 lambda introducer ,其中存放要捕获的外部变量表,外部变量要注意区分值传递和引用传递。如果里面放一个等号:[=, &y]
表示接收以值传递的形式接收所有的外界变量,不太建议用,要把自己用到的变量写清楚。()
中存放 lambda 函数的参数列表{}
是 lambda 函数的函数体- 中间的三项(标明 opt 的)都是看情况可有可无的,但是一旦三个中有一个是出现的,那么小括号
()
就必须有;而若三个可选项都没有,则()
也是可有可无的。 - mutable 指明参数是否可被改变,throwSpec 指明是否可能会抛出异常,retType 指明返回值的类型
- lambda 函数默认是内联的
举例:
#include <iostream>
int main() {
int id = 0;
auto f = [id] () mutable {
std::cout << "id: " << id << std::endl;
++id;
};
id = 42;
f();
f();
f();
std::cout << id << std::endl;
}
输出:
id: 0
id: 1
id: 2
42
注意:
- 在定义 lambda 函数
f()
时,就已经把 id 以值传递的形式传给函数,因此后面 id 的改变不会影响函数真正被调用时的 id 值 - 不加
mutable
关键字会报 id 是只读变量,不能修改。
varidic template 变参模板详解
原视频这里花了很大篇幅来讲解变参模板及其应用这个极其重要的新特性,但是考虑到在新手日常编程中的使用并不是太多(而多是出现在大型模板库的设计中),这里暂时略过,以后再回来补。
Ref:
https://blog.csdn.net/weixin_38339025/article/details/89161324