《C++ Primer Plus》18. 探讨 C++ 新标准

回顾前面介绍过的 C++ 11 功能

long longunsigned long long

char16_tchar32_t

原始字符串 R"..."

初始化列表

std::initializer_list

autodecltype、返回类型后置、模板别名(using =

智能指针

异常规范、noexcept

explicit

类内成员初始化(在类定义中初始化成员),但只能使用等号或大括号版本的初始化

基于范围的 for 循环

右值引用

移动语义(Move Semantics)和右值引用

在原来的 C++ 中,只有拷贝构造函数,对于下面的代码:

vector<string> allcaps(const vector<string>& vs) {
    vector<string> temp;
    ...
    return temp;
}

vector<string> vstr;
vector<string> vstr_copy1(vstr);          // #1
vector<string> vstr_copy2(allcaps(vstr)); // #2

在上面的代码的 #2 中,allcaps(vstr) 创建了对象 temp 临时对象被返回,交给了 vector<string> 的拷贝构造函数,多了一次拷贝构造

显然,直接将 allcaps(vstr) 的结果所有权转让给 vstr_copy2 更好

这种方法被称为移动语义

移动语义的一个实践是构造函数,移动构造函数的定义像下面这样:

class Useless {
private:
    int n;
    char* pc;
    
public:
    ...
    Useless::Useless(Useless&& f);
};

Useless::Useless(Useless&& f) : n(f.n) {
    pc = f.pc;
    
    f.pc = nullptr;
    f.n = 0;
}

移动构造函数窃取(pilfering)了所有权,并将原来的对象清空

使用左值时,将调用拷贝构造函数,使用右值时调用移动构造函数:

Useless two = one;             // 拷贝
Useless four(one + three);     // 移动

移动语义也适用于赋值运算符:

class Useless {
private:
    int n;
    char* pc;
    
public:
    ...
    Useless& operator=(Useless&& f);
};

Useless& Useless::operator=(Useless&& f) {
    if (this == &f) {
        return *this;
    }
    
    delete [] pc;
    n = f.n;
    pc = f.pc;
    
    f.pc = nullpr;
    f.n = 0;
    
    return *this;
}

移动构造函数和移动赋值使用右值,如果我们想要使用左值,需要强制移动:

Useless one;
Useless two = std::move(one);

此时,如果 Useless 定义了移动赋值,那么就会强制调用移动赋值运算符

新的类功能

特殊的成员函数

在原有的 4 个特殊成员函数(默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数)的基础上,C++ 11 新增了两个:移动构造函数和移动赋值运算符。这些成员函数时编译器在各种情况下自动提供的

默认的移动构造函数和移动赋值运算符与拷贝版本相似:逐成员初始化并移动,如果成员没有移动构造或赋值运算符,那么执行拷贝

注意默认的移动构造函数和移动赋值运算符遇到指针成员时,不会将源指针设置为 nullptr,因此可能导致悬垂指针的问题

默认方法和禁用方法

可以使用 delete 禁止编译器使用特定方法,例如:

class Some {
public:
    void redo(double);
    void redo(int) = delete;
};

int main() {
    Some s;
    s.redo(5);
}

上面的代码会报错,因为 redo(int) 被标注为 delete

可以显示标注 default 来让编译器提供默认的成员函数(只适用于 6 个特殊成员函数)

委托构造函数

C++ 11 允许在一个构造函数的定义中使用另一个构造函数,如:

class Notes {
    int k;
    double x;
public:
    Notes();
    Notes(int);
    Notes(int, double);
};

Notes::Notes() : Notes(1, 0.1) {}
Notes::Notes(int k_) : Notes(k_, 0.1) {}
Notes::Notes(int k_, double x_) : k(k_), x(x_) {}

这种情况叫做委托构造

继承构造函数

C++ 11 提供了一种让派生类能够继承基类构造函数的机制,如:

class BS {
public:
    BS();
    BS(int k);
    BS(double x);
    BS(int k, double x);
};

class DR : public BS {
public:
    using BS::BS;
    DR();
    DR(int k);
    DR(double x);
};

int main() {
    DR dr1;
    DR dr2(1);
    DR dr3(0.1);
    DR dr4(1, 0.1);
}

上面的代码中,DR 继承了 BS 的所有构造函数,但是其中三个被覆盖了,所以只有 dr4 使用了 BS 的构造函数

管理虚方法:overridefinal

在虚方法中,如果子类的某个成员函数与父类同名,但是函数签名不同,那么并不会覆盖父类的版本,而是会隐藏父类的版本,如:

class Action {
public:
    virtual void f(char ch) const;
};

class Bingo {
public:
    virtual void f(char* ch) const;
};

int main() {
    Bingo b(10);
    b.f('@');
}

上面的代码将报错

在 C++ 11 中可以使用虚说明符 override 表明要覆盖一个虚函数,如:

virtual void f(char* ch) const override;

现在,编译器在创建类时就会报错

如果不想要子类覆盖特定的虚方法,可以使用 final,如:

virtual void f(char* ch) const final;

注意,overridefinal 并非关键字,而是具有特殊意义的标识符,在正常情况下是可以用它做变量名的

Lambda 函数

lambda 这个名称来自 lambda calculus(lambda 演算)

可以把 lambda 函数看成是一个函数对象,它可以捕获环境

包装器

C++ 提供了多个包装器(wrapper,也可以叫适配器 adapter)

这些对象用于给其他编程接口提供更一致或更合适的接口,例如前面的 bin1stbind2nd

C++ 11 提供了其他的包装器,包括:

  • 模板 bind:可以代替 bin1stbind2nd,更灵活
  • 模板 men_fn:能够将成员函数作为常规函数进行传递
  • 模板 reference_wrapper:能够创建行为像引用但可以被复制的对象
  • 包装器 function:能够以统一的方式处理多种类似于函数的形式

包装器 function

可调用类型(callable type)很多,可能导致模板的效率极低,如:

template <typename T, typename F>
T use_f(T v, F f) { ... }

class Fp {
public:
    double operator()(double q) { ... }
}; // 函数对象
class Fq {
public:
    double operator()(double q) { ... }
}; // 函数对象

double dub(double x) { ... }
double square(double x) { ... }

int main() {
    double x = 1.1;
    use_f(x, dub);
    use_f(x, square);
    use_f(x, Fp());
    use_f(x, Fq());
    use_f(x, [](double u) { return u * u; });
    use_f(x, [](double u) { return u + u / 2.0; });
}

上面的代码将会实例化 5 个 use_f 模板(dubsqure 类型相同)

function 包装器可以解决这个问题,上面的代码可以改成:

std::function<double(double)> ef1 = dub;
std::function<double(double)> ef2 = square;
std::function<double(double)> ef3 = Fp();
std::function<double(double)> ef4 = Fq();
std::function<double(double)> ef5 = [](double u) { return u * u; };
std::function<double(double)> ef6 = [](double u) { return u + u / 2.0; };

这样的话就只实例化了一次,function 包装器的类型参数是 return_type(arg1_type, arg2_type, ...)

也可以更改函数模板:

template <typename T, typename F>
T use_f(T v, std::function<T(T)> f) { ... }

可变参数模板(variadic template)

可变参数模板让我们可以创建这样的模板函数和模板类:可以接受可变数量的参数

要理解这几个要点:

  • 模板参数包(parameter pack)
  • 函数参数包
  • 展开(unpack)参数包
  • 递归

模板和函数参数包

C++ 11 提供了一个用省略号表示的元运算符(meta-operator)

template<typename... Args>
void show_list(Args... args) {
    ...
}

其中,Args 是一个模板参数包,args 是一个函数参数包

这个函数可以与 0 或多个参数的函数调用匹配

递归与展开参数包

我们可以通过递归来展开参数包,如:

template<typename T, typename... Args>
void show_list_(T value, Args... args) {
    // 对 value 进行操作
    show_list_(args...); // 将剩下的东西继续传递
}

// 定义 0 个参数的函数,用于终止递归
void show_list() {
    ...
}

可以指定展开模式(pattern),例如改成引用传递:

template<typename T, typename... Args>
void show_list_(T value, const Args&... args) { ... }

这样会对每个函数参数应用模式 const Type&

C++ 11 新增的其他功能

线程相关:

  • thread_local:持续性与线程相关的静态存储
  • 原子操作库提供了头文件 atomic
  • 线程支持库提供了头文件 thread、mutex、condition_variable 和 future

新增的专用库:

  • random 头文件:提供了大量比 rand() 复杂的随机数工具
  • chrono 头文件:提供了处理时间间隔的途径
  • tuple 头文件:元组
  • ratio 头文件:编译阶段有理数算术库
  • regex 头文件:正则表达式

低级编程:

  • 放松了 POD(Plain Old Data)的要求
  • 允许结构体有构造函数和析构函数,但是不能有虚函数
  • alignofalignas 可以查看和更改对齐方式
  • constexpr 编译期计算表达式

其他杂项:

  • 字面量运算符(literal operator):ReturnType operator"" _n(...)
  • 调试工具:assert 宏、static_assert
  • 加强了对元编程(metaprogramming)的支持