发布于 ,更新于 

C++ 语言特性杂谈与常数优化

起因是发现很多同学对常数优化和语句的执行效率有误解(我肯定也有,但是我尽量把我知道的正确的写出来),因此写一篇杂谈做一个说明,同时介绍一些比较新的好用的语言特性(C++14)

template 模板关键字

函数

template 顾名思义是用来做模板的,那是做什么的模板呢? 举个例子,一般来说我们写 max 函数替代 std::max 来优化的时候,写出来的大概长下面这样:

1
2
3
int max(int a, int b) {
return a > b ? a : b;
}

在这三行代码中,第一行定义了 max 的返回值和参数 a b 的类型为 int,显然意味着这个函数只能比较 int 类型的量。

那么如果既想要比较 int 又想要比较 long long 还想比较 char 等等,如果按传统的写法,就是 Ctrl+C Ctrl+V 写好几个比较函数,还得重新命名,甚是麻烦。

观察写出来的几个函数,肉眼可见他们的本质区别只有返回值与参数的类型。那么有没有一种类似模板的东西,写一个函数的主要部分作为模板,返回值、变量的类型由编译器决定呢?

这就是 template 的一大作用,也是 OI 中最常用的方面。

使用起来也很简单,在函数声明前面加上 template <typename T>,此处 T 可以是随便一个字母或者单词,后面写函数的时候把 int 替换成 T 即可。

1
2
3
4
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}

此时的 T 相当于一个变量类型,如果这个函数被调用,编译器会给你生成一个符合传入参数的类型的 max 函数。

当然了,如果是一个没有传参的函数,我们也能指定其类型。
这样写会报错:

1
2
3
4
5
6
7
8
template <typename T>
T read() {
T a;
cin >> a;
return a;
}

int a = read();

原因是没有传参进去,编译器无法推断模板函数的类型。
类比 STL 容器 中的使用方法,能不能用尖括号带一个类型名称进去呢?可以的。

1
2
3
4
5
6
7
8
template <typename T>
T read() {
T a;
cin >> a;
return a;
}

int a = read<int>();

这样写就不会报错了

模板类

和模板函数类似,我们可以泛化结构体中的类型。

1
2
3
struct qwq {
int age
};

存紫妈的年龄可能会爆 int存阿求的年龄可能浪费 long long 的空间,于是可以:

1
2
3
4
5
6
7
template <class T>
struct qwq {
T age;
};

qwq<int8_t> akyuu;
qwq<__int128> yukari;

这样,akyuu.age 的类型即是 int8_tyukari.age 的类型即是 __int128

TODO: ... 关键字

基于范围的循环

不过多介绍了,主要在 STL 里用

1
2
3
for (auto i : container) {
std::cout << i << ' ';
}

container 为容器的名称,auto 是 C++11 以后的类型名称,编译时由编译器推断。

此处 auto 的效果就是把 i 的类型设置为 container 的元素的类型。如果 containervector<int> ,则上面语句的效果就是遍历 container 的所有元素并输出。

此外,如果想遍历的同时修改容器中的元素,可以在 i 前加取址符 & 表示引用,如下

1
2
3
for (auto &i : container) {
i = 0;
}

该段代码的效果即为遍历容器 container,同时把其中所有元素赋值为 0;

函数相关

Lambda 表达式

实际上就是定义了一个函数。

基本语法:

1
2
3
[捕获](参数){
// some code...
}

捕获一般没啥用,留空就好。

Lambda 表达式在 C++ 中是一个类型,但是不能用类型名定义,所以用 auto 声明。
举个例子,编写 max 函数:
原来是这样的

1
2
3
int max(int a, int b) {
return a > b ? a : b;
}

用 Lambda 表达式长这样:

1
2
3
auto max = [](int a, int b) { // 捕获部分留空,传参和内部实现不变
return a > b ? a : b;
};

看起来这两种方式没有什么区别,确实是这样,但是 Lambda 还有一个重要性质——匿名。

显然,原来写函数的时候必须要声明,声明这个函数的名称,返回值类型,传参类型,内部实现。
但是观察到上面声明 Lambda 表达式 max 的时候,使用的是类似变量赋值的方法。那是不是只留下等式右边的部分也可以当函数使用?实际上是可以的,这就是 Lambda 函数的匿名性质。即引入 Lambda 表达式之后,就可以随时随地定义函数而不给它起名字。
听起来有点抽象,再举个例子:std::sort
原来我们想要自定义比较函数,只能这样:

1
2
3
4
5
6
7
bool cmp(int a, int b) {
return a > b;
}

...

std::sort(a.begin(), a.end(), cmp);

引入 Lambda 表达式之后:

1
std::sort(a.begin(), a.end(), [](int a, int b){return a > b;});

在这里,我们并没有给比较函数起名字,就直接扔到了 std::sort 里面,这样做方便很多~

读者可能有疑问:这样做效率如何?事实上,Lambda 表达式和下面的函数对象的效率都是比较高的,比第一种方式中 cmp 这种函数指针效率高。

函数对象

假定你已经学会了类/结构体的运算符重载。

观察函数组成 function(),它是不是非常像一个类型 function 搭配上了 () 这个运算符?太像了!

那我们是不是可以这样做:开一个对象,重载 () 这个运算符再当函数用?当然可以。
还是用 max 举例:

1
2
3
4
5
6
7
8
9
10
11
struct qwq {
int operator() (int a, int b){
return a > b ? a : b;
}
};

qwq max;

...

a = max(a, b);

非常自然,非常易懂。

效率

懒得测了。
可以参考下 这个博客

利用如上链接内方式测试的结果是:差不多。