ACM | 新版 C++ 特性
本文来自 OI wiki,转载仅作学习记录使用。
注意 :考虑到算法竞赛的实际情况,本文将不会全面研究语法,只会讲述在算法竞赛中可能会应用到的部分。
本文语法参照 C++11 标准。语义不同的将以 C++11 作为标准,C++14、C++17 的语法视情况提及并会特别标注。
auto 类型说明符
auto 类型说明符用于自动推导变量等的类型。例如:
auto a = 1;        // a 是 int 类型
auto b = a + 0.1;  // b 是 double 类型
基于范围的 for 循环
下面是 C++20 前 基于范围的 for 循环的语法:
for (range_declaration : range_expression) loop_statement
上述语法产生的代码等价于下列代码( __range 、 __begin 和 __end 仅用于阐释):
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 范围表达式
范围表达式是任何可以表示一个合适的序列(数组,或定义了 begin 和 end 成员函数或自由函数的对象)的表达式,或一个花括号初始化器列表。正因此,我们不应在循环体中修改范围表达式使其任何尚未被遍历到的“迭代器”(包括“尾后迭代器”)非法化。
这里有一个例子:
for (int i : {1, 1, 4, 5, 1, 4}) std::cout << i;
loop_statement 循环语句
循环语句可以是任何语句,常为一条复合语句,它是循环体。
这里有一个例子:
#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 表达式是能够捕获作用域中的变量的无名函数对象,我们可以将其理解为一个匿名的内联函数。下面是 Lambda 表达式的语法:
[capture] (parameters) mutable -> return-type {statement}
capture 捕获子句
Lambda 表达式以 capture 子句开头,它指定哪些变量被捕获,以及捕获是通过值还是引用:有 & 符号前缀的变量通过引用访问,没有该前缀的变量通过值访问。空的 capture 子句 [] 指示 Lambda 表达式的主体不访问封闭范围中的变量。
我们也可以使用默认捕获模式: & 表示捕获到的所有变量都通过引用访问, = 表示捕获到的所有变量都通过值访问。之后我们可以为特定的变量 显式 指定相反的模式。
例如 Lambda 体要通过引用访问外部变量 a 并通过值访问外部变量 b ,则以下子句等效:
- [&a, b]
- [b, &a]
- [&, b]
- [b, &]
- [=, &a]
- [&a, =]
默认捕获时,会捕获 Lambda 中提及的变量。
parameters 参数列表
大多数情况下类似于函数的参数列表,例如:
auto lam = [](int a, int b) { return a + b; };
std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl;
C++14 中,若参数类型是泛型,则可以使用 auto 声明类型:
auto lam = [](auto a, auto b)
一个例子:
int x[] = {5, 1, 7, 6, 1, 4, 2};
std::sort(x, x + 7, [](int a, int b) { return (a > b); });
for (auto i : x) std::cout << i << " ";
这将打印出 x 数组从大到小排序后的结果。
mutable 可变规范
利用可变规范,Lambda 表达式的主体可以修改通过值捕获的变量。若使用此关键字,则 parameters 不可省略 (即使为空)。
return-type 返回类型
若 Lambda 主体只包含一个 return 语句或不返回值,则可以省略此部分。若 Lambda 表达式主体包含一个 return 语句,则返回类型将被自动推导,返回类型遵循 parameters(除非你想指定一个)。否则编译器会将返回类型推断为 void 。
例如,上文的 lam 也可以写作
auto lam = [](int a, int b) -> int
再举两个例子
auto x1 = [](int i) { return i; };  // OK
auto x2 = [] { return {1, 2}; };    // ERROR: 返回类型被推导为 void
statement Lambda 主体
Lambda 主体可包含任何函数可包含的部分。普通函数和 Lambda 表达式主体均可访问以下变量类型:
- 从封闭范围捕获变量
- 参数
- 本地声明的变量
- 在一个 class中声明时,捕获this
- 具有静态存储时间的任何变量,如全局变量
下面是一个例子
#include <iostream>
int main() {
  int m = 0, n = 0;
  [&, n](int a) mutable { m = (++n) + a; }(4);
  std::cout << m << " " << n << std::endl;
  return 0;
}
最后我们得到输出 5 0 。这是由于 n 是通过值捕获的,在调用 Lambda 表达式后仍保持原来的值 0 不变。 mutable 规范允许 n 在 Lambda 主体中被修改,将 mutable 删去则编译不通过。
decltype 说明符
decltype 说明符可以推断表达式的类型。
#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 。来看一个例子
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 的推广。来看一个例子:
#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的内容 | 
例子
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算法 | 
例子
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 异常。
来看例子
#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 开始支持这一特性。可变参数宏允许宏定义可以拥有可变参数,例如:
#define def_name(...) def_body(__VA_ARGS__)
其中, ... 是缺省符号, __VA_ARGS__ 在调用时会替换成实际的参数列表, def_body 应为可变参数模板函数。
现在就可以这么调用 def_name :
def_name();
def_name(1);
def_name(1, 2, 3);
def_name(1, 0.0, "abc");
可变参数模板
在 C++11 之前,类模板和函数模板都只能接受固定数目的模板参数。C++11 允许 任意个数、任意类型 的模板参数。
可变参数模板类
例如,下列代码声明的模板类 tuple 的对象可以接受任意个数、任意类型的模板参数作为它的模板形参。
template <typename... Values>
class Tuple {};
其中, Values 是一个模板参数包,表示 0 个或多个额外的类型参数。模板类只能含有一个模板参数包,且模板参数包必须位于所有模板参数的最右侧。
所以,可以这么声明 tuple 的对象:
Tuple<> test0;
Tuple<int> test1;
Tuple<int, int, int> test2;
Tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> test3;
如果要限制至少有一个模板参数,可以这么定义模板类 tuple :
template <typename First, typename... Rest>
class Tuple {};
可变参数模板函数
同样的,下列代码声明的模板函数 fun 可以接受任意个数、任意类型的模板参数作为它的模板形参。
template <typename... Values>
void fun(Values... values) {}
其中, Values 是一个模板参数包, values 是一个函数参数包,表示 0 个或多个函数参数。模板函数只能含有一个模板参数包,且模板参数包必须位于所有模板参数的最右侧。
所以,可以这么调用 fun 函数:
fun();
fun(1);
fun(1, 2, 3);
fun(1, 0.0, "abc");
参数包展开
之前说面了如何声明模板类或者模板函数,但是具体怎么使用传进来的参数呢?这个时候就需要参数包展开。
对于模板函数而言,参数包展开的方式有递归函数方式展开以及逗号表达式和参数列表方式展开。
对于模板类而言,参数包展开的方式有模板递归方式展开和继承方式展开。
递归函数方式展开参数包
递归函数方式展开参数包需要提供展开参数包的递归函数和参数包展开的终止函数。
举个例子,下面这个代码段使用了递归函数方式展开参数包,实现了可接受大于等于 2 个参数的取最大值函数。
// 递归终止函数,可以是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 的调试功能,而是喜欢输出中间变量。但是,有时候要输出的中间变量数量有点多,写输出中间变量的代码的时候可能会比较烦躁,这时候就可以用上可变参数模板和可变参数宏。
// Author: Backl1ght
#include <bits/stdc++.h>
using namespace std;
namespace DEBUG {
template <typename T>
inline void _debug(const char* format, T t) {
  cerr << format << '=' << t << endl;
}
template <class First, class... Rest>
inline 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 的相关代码,后续输出中间变量的时候就会方便许多。
参考
ACM | 新版 C++ 特性的更多相关文章
- 最新版CentOS8系统安装和基本配置
		一.Centos8新版简介 Redhat公司在2019年5月推出了rhel8,年底推出了centos8,紧接着也会把相关的技术认证完全转换为新平台. 阿里云.华为云目前也已推出centos8的公共镜像 ... 
- EventBus   (四)   Sticky事件
		什么是Sticky事件? 关于Sticky事件有的同学可能不是很熟悉,Sticky的意思是粘性的.在Android开 发中,Sticky事件只指事件消费者在事件发布之后才注册的也能接收到该事件的特殊类 ... 
- cocos2d-x 3.0 开发(一) Hello_New_World
		1.Previous On 2dx 2dx 的3.0版本是个与以往不同的版本.变化比2dx从1.x 到2.x的变化还要大不少.具体的新功能可以参见:CocoaChina大会见闻——cocos2 ... 
- Web 建站技术中,HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、ASP.NET、Web Services 是什么(转)
		Web 建站技术中,HTML.HTML5.XHTML.CSS.SQL.JavaScript.PHP.ASP.NET.Web Services 是什么?修改 建站有很多技术,如 HTML.HTML5.X ... 
- Material Design In Action——重构bilibili客户端
		前言 哔哩哔哩动画是中国大陆的一家弹幕视频网站,在中国二次元用户中颇受欢迎. 哔哩哔哩动画之前推出过采用 Android Design 的 Android 客户端,虽然有使用了部分过时控件(例如 Sc ... 
- 程序安装制作不用愁—Wise installation入门教程
		http://blog.csdn.net/terryzero/article/details/6731925最近有个项目需要把别人的工具包装集成下,所以就随便找了个制作安装的工具,正好找到了Wise ... 
- 管理 node 版本,选择 nvm 还是 n?
		来源:http://taobaofed.org/blog/2015/11/17/nvm-or-n/ 引子 我本机安装着 nvm,而 node 本来一直运行在 0.x 的老版本上.后来为了跑 ES6,我 ... 
- druid0.15.0安装方式
		Druid0.15.0安装文档 1 集群规划 Master包含Coordinator和Overlord,4核16G*2: data包含Historical和MiddleManager,16核64G*3 ... 
- 扫描神器nmap使用教程
		总结 nmap -v 详细信息输出nmap -p 指定端口nmap -iL 扫描文件中的ipnmap -exclude 不扫描某些ipnmap -Pn 使用ping扫描,显式地关闭端口扫描,用于主机发 ... 
- Liferay-Activiti 企业特性功能介绍 (新版Liferay7)
		前言 如果你是开发者 你已经是多少次开发一个项目,一次次的用一些框架,一次次的写类似的重复的代码,一次次建表\写类和方法\写HTML\CSS\JAVASCRIPT,一次次测试,一次次的写Bug...如 ... 
随机推荐
- 期望最大化(EM)算法:从理论到实战全解析
			本文深入探讨了期望最大化(EM)算法的原理.数学基础和应用.通过详尽的定义和具体例子,文章阐释了EM算法在高斯混合模型(GMM)中的应用,并通过Python和PyTorch代码实现进行了实战演示. 关 ... 
- 做数据分析,我们需要懂多少excel知识?
			数据分析所需的Excel知识详解 在进行数据分析工作时,Excel是一个非常常用且强大的数据处理工具.以下是数据分析中常用的Excel知识点和技巧的详细描述. 1. 基本操作 在使用Excel进行数据 ... 
- mysql之慢sql配置与分析
			mysql的慢查询sql是通过日志记录慢SQL--(俗称慢查询日志)默认的情况下,MySQL数据库不开启慢查询日志(slow query log),需要手动把它打开 开启慢查询日志 SET GLOBA ... 
- 【VMware vSAN】主机之间网络性能测试,提示“无法运行网络性能测试。请稍后重试。”的处理过程。
			vSAN集群监控,有一个主动测试功能,里面可以针对vSAN主机进行虚拟机创建测试.网络性能测试等. 官方解释: 虚拟机创建测试通常需要 20 至 40 秒时间,在超时情况下最长需要 180 秒时间.将 ... 
- ubuntu 22.04搭建本地源
			ubuntu 22.04搭建本地源 基于服务器无法联网的情况下,从一台可以上网的服务器下载软件包和依赖,再在无法联网的服务器中搭建本地源,安装环境. 下载(包括依赖): cd /root mkdir ... 
- django-filter的详细使用
			有时候前端需要各种各样的过滤查询,如果自己写多少有点麻烦和冗余.使用django-filter就可以很好的解决这个问题. django-filter可以用在django上, 也与配合drf一起使用. ... 
- C 语言教程:条件和 if...else 语句
			C 语言中的条件和 if...else 语句 您已经学习过 C 语言支持数学中的常见逻辑条件: 小于:a < b 小于或等于:a <= b 大于:a > b 大于或等于:a > ... 
- NetSuite 开发日记:批量增删改
			一.批量插入/创建 使用 record.create() 插入数据时,一次只能插入一条,有多条数据需要插入时只能通过循环的方式,这样效率非常慢,耗时会随着数据量的增大而呈线性增长,有一种巧妙的方式可以 ... 
- Javascript面向对象的程序设计 —— 自定义类实现继承的6种方法
			许多OO语言都支持两种继承方式: 接口继承:只继承方法签名: 实现继承:继承实际的方法.ECMAScript只支持实现继承,实现继承是继承实际的方法,依靠原型链来实现.1.原型链原型链是实现继承的主要 ... 
- 当一名阿里P9是什么样的体验?
			今天斗胆来跟大家分享一下作为一位阿里的P9是什么样的体验? 首先还是简单给大家介绍一下阿里的p9是属于一个什么样的层级?阿里p9中文名称叫做资深技术专家,一般大概带的团队有从几十人到一两百号人不等,具 ... 
