C++11新特性选讲 语言部分 侯捷

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) {
  /* --- */
}

变参模板的花式应用

  1. 万能的hash function:多种函数重载 + 递归 + 函数变参模板 —>花式调用

在这里插入图片描述

  1. 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 均无法实现上面的效果。

  1. 若使用宏定义:

    #define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;
    

    我们知道宏定义就是机械地字符替换,所以在使用时:

    Vec<int> coll;
    // 等价于
    template<typename int> std::vector<int, MyAlloc<int>>;
    

    完全不是我们想要的意思。

  2. 若使用 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关键字总结

  1. using-directives,如 using namespace std;

  2. using-declarations for namespace members,如 using std::cout;

  3. using-declarations for class members,如 using _Base::_M_allocate;

  4. type alias (since C++11),如:

    template <typename T>
    using Vec = std::vector<T, MyAlloc<T>>;
    
  5. 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

  1. 修饰类,使得该类不能被继承

    struct Base final {};
    
    struct Derived: Base {};			// Error
    

    Base 类被 final 关键字修饰,使得其不能被继承,下面的 Derived 试图继承它,会报错。

  2. 修饰虚函数,使得该虚函数不能被重写

    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

注意:

  1. 在定义 lambda 函数 f() 时,就已经把 id 以值传递的形式传给函数,因此后面 id 的改变不会影响函数真正被调用时的 id 值
  2. 不加 mutable 关键字会报 id 是只读变量,不能修改。

varidic template 变参模板详解

原视频这里花了很大篇幅来讲解变参模板及其应用这个极其重要的新特性,但是考虑到在新手日常编程中的使用并不是太多(而多是出现在大型模板库的设计中),这里暂时略过,以后再回来补。

Ref:

https://blog.csdn.net/weixin_38339025/article/details/89161324

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值