modern c++知识点整理(一):语言的可用性强化

272 阅读8分钟

1. 被弃用的特性

  • 不再允许字符串字面值常量赋值给一个 char *。如果需要用字符串字面值常量赋值和初始化一个char *,应该使用 const char * 或者 auto。
char *str = "hello world!"; // 将出现弃用警告
  • C++98 异常说明、unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用noexcept。
  • auto_ptr 被弃用,应使用 unique_ptr。 • register 关键字被弃用,可以使用但不再具备任何实际含义。
  • bool 类型的 ++ 操作被弃用。
  • 如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
  • C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_cast、reinterpret_cast、const_cast 来进行类型转换。
  • 特别地,在最新的 C++17 标准中弃用了一些可以使用的 C 标准库,例如 、、 与 等

2.语言可用性的强化

2.1 常量

nullptr

nullptr出现的目的是为了替代NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为0。

c++11引入了 nullptr 关键字,专门用来区分空指针、0。

2.2 变量及其初始化

if/switch 变量声明强化

在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 if 和 switch 语句中声明一个临时的变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成变量的定义:

// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4; }

###初始化列表

初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。例如

#include <iostream>
#include <vector>

class Foo {
    public:
        int value_a;
        int value_b;
        Foo(int a,int b) : value_a(a),value_b(b) {}
};

int main(){
    // before C++11
    int arr[3] = {1, 2, 3};
    Foo foo(1, 2);
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::cout << "arr[0]: " << arr[0] << std::endl;
    std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;
    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
        std::cout<< *it << std::endl;
    }
    return 0;
}

为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

#include <initializer_list>
#include <vector>
class MagicFoo{
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
        for (std::initializer_list<int>::iterator it = list.begin(); it != list.end();++it)
            vec.push_back(*it);
    }
};
int main(){
    //after C++11
    MagicFoo magicFoo = {1,2,3,4,5};
    ......
}

结构化绑定

结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。

C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:

#include <iostream>
#include <tuple>
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}
int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << std::endl;
return 0; }

2.3类型推导

auto

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并 存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变 更也就非常自然了。 注意:auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们 应该使用模板);此外,auto 还不能用于推导数组类型。

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:decltype(表达式)

2.4 控制流

区间for迭代

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
    std::vector<int> vec = {1, 2, 3, 4};
    if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
    for (auto element : vec)
        std::cout << element << std::endl; // read only
    for (auto &element : vec) {
        element += 1; // writeable
    }
    for (auto element : vec)
        std::cout << element << std::endl; // read only
}

类型别名模板

在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话:模板是用来 产生类型的。在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一 个新的名称。因为,模板不是类型。例如:

template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};

typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
int main() {
TrueDarkMagic<bool> you;
}

默认模板参数

我们可能定义了一个加法函数:

// c++11 version
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
// Call add function
auto ret = add<int, int>(1,3);

但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。 在 C++11 中提供了一种便利,可以指定模板的默认参数:

template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
// Call add function
auto ret = add(1,3);

变长参数模板

模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。在 C++11 之前,无论是类模板 还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方 法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。

template<typename... Ts> class Magic;

模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:

class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;

既然是任意形式,所以个数为 0 的模板参数也是可以的:class Magic<> nothing;

如果不希望产生的模板参数个数为 0,可以手动的定义至少一个模板参数:

template<typename Require, typename... Args> class Magic;

变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数,虽然也能达成不定个数 的形参的调用,但其并非类别安全。而 C++11 除了能定义类别安全的变长参数函数外,还可以使类似 printf 的函数能自然地处理非自带类别的对象。除了在模板参数中能使用 ... 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数,这也就为我们简单编写变长参数函数提供了便捷的手段, 例如:

template<typename... Args> void printf(const std::string &str, Args... args);

那么我们定义了变长的模板参数,如何对参数进行解包呢? 首先,我们可以使用 sizeof... 来计算参数的个数,:

template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以传递任意个参数给 magic 函数:
magic(); // 输出 0
magic(1); // 输出 1
magic(1, ""); // 输出

其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理 手法:

  1. 递归模板函数

递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参 数,进而达到递归遍历所有模板参数的目的:

#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0; }
  1. 变参模板展开

你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完 成 printf 的编写:

template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}

事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用 std::bind 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。

  1. 初始化列表展开

递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。 这里介绍一种使用初始化列表展开的黑魔法:

template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}

在这个代码中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性(下文中将 提到)。 通过初始化列表,(lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行 前面的 lambda 表达式,完成参数的输出。为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void。

2.6 面向对象

委托构造

C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函 数,从而达到简化代码的目的:

#include <iostream>
class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1; }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};
int main() {
    Base b(2);
    //1,2
    std::cout << b.value1 << std::endl;
    std::cout << b.value2 << std::endl;
}

继承构造

在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利 用关键字 using 引入了继承构造函数的概念:

#include <iostream>
class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1; }
    Base(int value) : Base() { // 委托 Base() 构造函数
        value2 = value;
    }
};
class Subclass : public Base {
public:
    using Base::Base; // 继承构造
};

int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}

显示虚函数重载

在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:

struct Base {
virtual void foo();
};
struct SubClass: Base {
void foo();
};

SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另 一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成 为了一个普通的类方法,这将造成灾难性的后果。

C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。

override 当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函 数是否存在这样的虚函数,否则将无法通过编译:

struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};

final final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};

显式禁用默认函数

在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、复制构造、赋值 算符以及析构函数。另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。当程序员有需要时, 可以重载这部分函数。 这就引发了一些需求:无法精确控制默认函数的生成行为。例如禁止类的拷贝时,必须将复制构造 函数与赋值算符声明为 private。尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不 优雅的方式。 并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。若用户定义了任何构造函 数,编译器将不再生成默认构造函数,但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。 C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。例如:

class Magic {
public:
Magic() = default; // 显式声明使用编译器生成的构造
Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
Magic(int magic_number);
}