跳转至

新版 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

另請參閲 新版 C++ 特性: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