跳转至

新版 C++ 特性

注意:考虑到算法竞赛的实际情况,本文将不会全面研究语法,只会讲述在算法竞赛中可能会应用到的部分。

本文语法参照 C++11 标准。语义不同的将以 C++11 作为标准,C++14、C++17 的语法视情况提及并会特别标注。

auto 类型说明符

auto 类型说明符用于自动推导变量等的类型。例如:

1
2
auto a = 1;        // a 是 int 类型
auto b = a + 0.1;  // b 是 double 类型

基于范围的 for 循环

下面是 C++20 前 基于范围的 for 循环的语法:

1
for (range_declaration : range_expression) loop_statement

上述语法产生的代码等价于下列代码(__range__begin__end 仅用于阐释):

1
2
3
4
5
auto&& __range = range_expression;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
  range_declaration = *__begin;
  loop_statement
}

range_declaration 范围声明

范围声明是一个具名变量的声明,其类型是由范围表达式所表示的序列的元素的类型,或该类型的引用。通常用 auto 说明符进行自动类型推导。

range_expression 范围表达式

范围表达式是任何可以表示一个合适的序列(数组,或定义了 beginend 成员函数或自由函数的对象)的表达式,或一个花括号初始化器列表。正因此,我们不应在循环体中修改范围表达式使其任何尚未被遍历到的「迭代器」(包括「尾后迭代器」)非法化。

这里有一个例子:

1
for (int i : {1, 1, 4, 5, 1, 4}) std::cout << i;

loop_statement 循环语句

循环语句可以是任何语句,常为一条复合语句,它是循环体。

这里有一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

struct C {
  int a, b, c, d;

  C(int a = 0, int b = 0, int c = 0, int d = 0) : a(a), b(b), c(c), d(d) {}
};

int* begin(C& p) { return &p.a; }

int* end(C& p) { return &p.d + 1; }

int main() {
  C n = C(1, 9, 2, 6);
  for (auto i : n) std::cout << i << " ";
  std::cout << std::endl;
  // 下面的循环与上面的循环等价
  auto&& __range = n;
  for (auto __begin = begin(n), __end = end(n); __begin != __end; ++__begin) {
    auto ind = *__begin;
    std::cout << ind << " ";
  }
  std::cout << std::endl;
  return 0;
}

Lambda 表达式

详见 Lambda 表达式 页面。

decltype 说明符

decltype 说明符可以推断表达式的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <vector>

int main() {
  int a = 1926;
  decltype(a) b = a / 2 - 146;         // b 是 int 类型
  std::vector<decltype(b)> vec = {0};  // vec 是 std::vector <int> 类型
  std::cout << a << vec[0] << b << std::endl;
  return 0;
}

constexpr

constexpr 说明符声明可以在编译时求得函数或变量的值。其与 const 的主要区别是一定会在编译时进行初始化。用于对象声明的 constexpr 说明符蕴含 const,用于函数声明的 constexpr 蕴含 inline。来看一个例子

1
2
3
4
5
6
int fact(int x) { return x ? x * fact(x - 1) : 1; }

int main() {
  constexpr int a = fact(5);  // ERROR: 函数调用在常量表达式中必须具有常量值
  return 0;
}

int fact(int x) 之前加上 constexpr 则编译通过。

std::tuple

std::tuple 定义于头文件 <tuple>,是固定大小的异类值汇集(在确定初始元素后不能更改,但是初始元素能有任意多个)。它是 std::pair 的推广。来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <tuple>
#include <vector>

constexpr auto expr = 1 + 1 * 4 - 5 - 1 + 4;

int main() {
  std::vector<int> vec = {1, 9, 2, 6, 0};
  std::tuple<int, int, std::string, std::vector<int> > tup =
      std::make_tuple(817, 114, "514", vec);
  std::cout << std::tuple_size<decltype(tup)>::value << std::endl;

  for (auto i : std::get<expr>(tup)) std::cout << i << " ";
  // std::get<> 中尖括号里面的必须是整型常量表达式
  // expr 常量的值是 3,注意 std::tuple 的首元素编号为 0,
  // 故我们 std::get 到了一个 std::vector<int>
  return 0;
}

成员函数

函数作用
operator=赋值一个 tuple 的内容给另一个
swap交换二个 tuple 的内容

例子

1
2
3
4
constexpr std::tuple<int, int> tup = {1, 2};
std::tuple<int, int> tupA = {2, 3}, tupB;
tupB = tup;
tupB.swap(tupA);

非成员函数

函数作用
make_tuple创建一个 tuple 对象,其类型根据各实参类型定义
std::get元组式访问指定的元素
operator==按字典顺序比较 tuple 中的值
std::swap特化的 std::swap 算法

例子

1
2
3
4
std::tuple<int, int> tupA = {2, 3}, tupB;
tupB = std::make_tuple(1, 2);
std::swap(tupA, tupB);
std::cout << std::get<1>(tupA) << std::endl;

std::function

类模板 std::function 是通用多态函数封装器,定义于头文件 <functional>std::function 的实例能存储、复制及调用任何可调用(Callable)目标——函数、Lambda 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。

存储的可调用对象被称为 std::function目标。若 std::function 不含目标,则称它为 。调用空 std::function 的目标将导致抛出 std::bad_function_call 异常。

来看例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <functional>
#include <iostream>

struct Foo {
  Foo(int num) : num_(num) {}

  void print_add(int i) const { std::cout << num_ + i << '\n'; }

  int num_;
};

void print_num(int i) { std::cout << i << '\n'; }

struct PrintNum {
  void operator()(int i) const { std::cout << i << '\n'; }
};

int main() {
  // 存储自由函数
  std::function<void(int)> f_display = print_num;
  f_display(-9);

  // 存储 Lambda
  std::function<void()> f_display_42 = []() { print_num(42); };
  f_display_42();

  // 存储到成员函数的调用
  std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
  const Foo foo(314159);
  f_add_display(foo, 1);
  f_add_display(314159, 1);

  // 存储到数据成员访问器的调用
  std::function<int(Foo const&)> f_num = &Foo::num_;
  std::cout << "num_: " << f_num(foo) << '\n';

  // 存储到函数对象的调用
  std::function<void(int)> f_display_obj = PrintNum();
  f_display_obj(18);
}

可变参数宏

可变参数宏是 C99 引入的一个特性,C++ 从 C++11 开始支持这一特性。可变参数宏允许宏定义可以拥有可变参数,例如:

1
#define def_name(...) def_body(__VA_ARGS__)

其中,... 是缺省符号,__VA_ARGS__ 在调用时会替换成实际的参数列表,def_body 应为可变参数模板函数。

现在就可以这么调用 def_name

1
2
3
4
def_name();
def_name(1);
def_name(1, 2, 3);
def_name(1, 0.0, "abc");

可变参数模板

在 C++11 之前,类模板和函数模板都只能接受固定数目的模板参数。C++11 允许 任意个数、任意类型 的模板参数。

可变参数模板类

例如,下列代码声明的模板类 tuple 的对象可以接受任意个数、任意类型的模板参数作为它的模板形参。

1
2
template <typename... Values>
class Tuple {};

其中,Values 是一个模板参数包,表示 0 个或多个额外的类型参数。模板类只能含有一个模板参数包,且模板参数包必须位于所有模板参数的最右侧。

所以,可以这么声明 tuple 的对象:

1
2
3
4
Tuple<> test0;
Tuple<int> test1;
Tuple<int, int, int> test2;
Tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> test3;

如果要限制至少有一个模板参数,可以这么定义模板类 tuple

1
2
template <typename First, typename... Rest>
class Tuple {};

可变参数模板函数

同样的,下列代码声明的模板函数 fun 可以接受任意个数、任意类型的模板参数作为它的模板形参。

1
2
template <typename... Values>
void fun(Values... values) {}

其中,Values 是一个模板参数包,values 是一个函数参数包,表示 0 个或多个函数参数。模板函数只能含有一个模板参数包,且模板参数包必须位于所有模板参数的最右侧。

所以,可以这么调用 fun 函数:

1
2
3
4
fun();
fun(1);
fun(1, 2, 3);
fun(1, 0.0, "abc");

参数包展开

之前说面了如何声明模板类或者模板函数,但是具体怎么使用传进来的参数呢?这个时候就需要参数包展开。

对于模板函数而言,参数包展开的方式有递归函数方式展开以及逗号表达式和参数列表方式展开。

对于模板类而言,参数包展开的方式有模板递归方式展开和继承方式展开。

递归函数方式展开参数包

递归函数方式展开参数包需要提供展开参数包的递归函数和参数包展开的终止函数。

举个例子,下面这个代码段使用了递归函数方式展开参数包,实现了可接受大于等于 2 个参数的取最大值函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 递归终止函数,可以是0或多个参数。
template <typename T>
T MAX(T a, T b) {
  return a > b ? a : b;
}

// 展开参数包的递归函数
template <typename First, typename... Rest>
First MAX(First first, Rest... rest) {
  return MAX(first, MAX(rest...));
}

// int a = MAX(1); // 编译不通过,但是对1个参数取最大值本身也没有意义
// int b = MAX(1, "abc"); //
// 编译不通过,但是在整数和字符串间取最大值本身也没有意义
int c = MAX(1, 233);              // 233
int d = MAX(1, 233, 666, 10086);  // 10086

可变参数模板的应用

举个应用的例子,有的人在 debug 的时候可能不喜欢用 IDE 的调试功能,而是喜欢输出中间变量。但是,有时候要输出的中间变量数量有点多,写输出中间变量的代码的时候可能会比较烦躁,这时候就可以用上可变参数模板和可变参数宏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Author: Backl1ght
#include <bits/stdc++.h>
using namespace std;

namespace DEBUG {
template <typename T>
void _debug(const char* format, T t) {
  cerr << format << '=' << t << endl;
}

template <class First, class... Rest>
void _debug(const char* format, First first, Rest... rest) {
  while (*format != ',') cerr << *format++;
  cerr << '=' << first << ",";
  _debug(format + 1, rest...);
}

template <typename T>
ostream& operator<<(ostream& os, const vector<T>& V) {
  os << "[ ";
  for (const auto& vv : V) os << vv << ", ";
  os << "]";
  return os;
}

#define debug(...) _debug(#__VA_ARGS__, __VA_ARGS__)
}  // namespace DEBUG

using namespace DEBUG;

int main(int argc, char* argv[]) {
  int a = 666;
  vector<int> b({1, 2, 3});
  string c = "hello world";

  // before
  cout << "a=" << a << ", b=" << b << ", c=" << c
       << endl;  // a=666, b=[ 1, 2, 3, ], c=hello world
  // 如果用printf的话,在只有基本数据类型的时候是比较方便的,然是如果要输出vector等的内容的话,就会比较麻烦

  // after
  debug(a, b, c);  // a=666, b=[ 1, 2, 3, ], c=hello world

  return 0;
}

这样一来,如果事先在代码模板里写好 DEBUG 的相关代码,后续输出中间变量的时候就会方便许多。

参考

  1. C++ reference
  2. C++ 参考手册
  3. C++ in Visual Studio
  4. Variadic template
  5. Variadic macros