《C++ PrimerPlus》 第十七章~第十八章

18.3 新的类功能

18.3.1 特殊的成员函数

18.3.2 默认的方法和禁用的方法

在C++中,如果只在程序中提供了移动构造函数,那么编译器将不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数。在这种情况下,可以使用关键字default显式的声明这些方法的默认版本:(即只给出函数头,后接=default,则这些方法的默认版本就会被创建)

1
2
3
4
5
6
7
8
class Someclass
{
public:
Someclass(Someclass &&);
Someclass() = default;
Someclass(const Someclass &) = default;
Someclass& operator=(const Someclass &) = default;
}

另一方面,关键字delete可用于禁止编译器使用特定的方法,例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符(之前是通过将其访问权限设值为private来实现的,现在的实现方法更容易理解,且不易犯错):

1
2


关键字default智能用于6个特殊的成员函数,但delete可用于任何成员函数。delete的一种可能用法是禁止特定的转换。

18.4 Lambda 函数

C++ 11 中提供了 Lambda 函数(表达式), 它们提供了一种有用的服务, 对使用函数谓词的 STL 算法来说尤其如此.

18.4.1 比较函数指针, 函数对象(仿函数)和 Lambda 函数

  1. 函数指针可以完成基本的函数功能, 并且可以作为参数传递给 STL 的算法函数.
  2. 仿函数本身是一个类的对象, 因此, 它不仅仅可以完成函数的功能, 还能在此基础上封装更多的信息, 以完成更加复杂的功能, 同样, 仿函数(类实例)也可以作为参数传递给接受函数指针为参数的函数.
  3. Lambda 表达式可以创建匿名函数, 即无需给函数命名, 在 C++11 中, 对于接受函数指针或仿函数的函数, 可以使用 lambda 表达式作为其参数.

lambda 表达式的返回类型是通过decltype根据返回值判断得到的, 如果 lambda 中不包含返回语句, 那么推断出的返回类型将为void. 注意, 仅当 lambda 表达式完全由一条返回语句组成时, 自动类型推断才管用, 否则, 需要使用新增的返回类型后置语法, 如下所示:

1
[](double x)->double {int y = x; return x - y;}; // 返回值类型为 double

18.4.2 为何使用 lambda

  • 距离: 让定义位于使用的地方附近, 这样就无需翻阅多页的源代码, 以了解函数的实际功能. 同时, 在修改代码时, 如果需要修改的代码涉及到的内容都在附近, 那么也会更加方便. 从这种角度看, 函数指针就无法满足这种要求, 因为不能在函数内部定义其他函数. 另外, 对于距离这一点, 仿函数也有同样的优势, 因为我们可以在函数的内部定义类.
  • 简洁: 相比于函数指针的定义和仿函数的定义来说, lambda 表达式的定义更加简洁, 我们可以给一个 lambda 表达式指定名称, 这样可以通过该名称多次使用对应的 lambda 表达式.
  • 效率: 通常, 函数指针会阻止内联, 因为编译器传统上不会内联其地址被获取的函数, 函数地址的概念意味着非内联函数. 而仿函数和 lambda 通常不会阻止内联.
  • 功能: lambda 表达式具有一些额外的功能, 具体地说, lambda 表达式可以访问作用域内的任何动态变量, 我们只要将需要的变量放在捕获括号里面即可.

18.6 可变参数模板

可变参数模板(variadic template)可以创建能够接受可变数量参数的模板函数和模板类. 要创建可变参数模板, 需要理解几个要点:

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

18.6.1 模板和函数参数包

C++11 提供了一个用省略号表示的元运算符(meta-operator), 让您能够声明表示模板参数包的标识符, 模板参数包基本上是一个类型列表. 同样, 它还让您能够声明表示函数参数包的标识符, 而函数参数包基本上是一个值列表. 其语法如下:

1
2
3
4
template<typename... Args> // Args 是一个模板参数包
void show_list(Args... args) { // args是一个函数参数包
...
}

上面的代码中, Args是一个模板参数包, 而args是一个函数参数包. ArgsT的差别在于, T与一种类型匹配, 而Args与任意数量(包括零)的类型匹配. 对于下面的函数调用:

1
show_list('S', 80, "sweet", 4.5);

在上面的情况下, 参数包Args包含与函数调用中的参数匹配的类型: char, int, const char*double. 我们可以用使用模板类型T一样使用Args, Args... args 意味着函数参数包args包含的值列表与模板参数包Args包含类型列表匹配—-无论是类型还是数量都匹配. 在上面的示例中, args包含值'S', 80, "sweet", 4.5.

18.6.2 展开参数包

我们无法通过索引来获取参数包中的参数, 也就是不能使用Args[2]来访问包中的第三个类型. 相反, 可将省略号放在函数参数包名的右边, 将参数包展开, 如下所示:

1
2
3
4
template<typename... Args>
void show_list(Args... args) {
show_list(args...); // 将展开后的 args 传递给 show_list, 但是这样会造成无限递归.
}

18.6.3 在可变参数模板函数中使用递归

展开参数后, 如果我们直接在show_list函数中进行展开, 那么它又会将新展开的参数作为参数包进行展开, 这样就会导致无限递归, 因此, 我们需要正确的使用递归来对参数包进行展开. 这里的核心理念是, 将函数参数包展开,对列表中的第一项进行处理, 再将余下的内容传递给递归调用, 以此类推, 直到列表为空. 与常规递归一样, 确保递归终止很重要(可通过添加一个只处理单项的模板来实现最后一项的特殊处理, 同时还可以起到结束递归的作用), 这里的技巧是将模板头改为如下所示:

1
2
template<typename T, typename... Args>
void show_list(T value, Args... args)

对于上述定义, 我们每次递归调用时, args都会变短, 这样, 最终就可以终止递归. 具体例子见p829.