函数作为最小的代码单元,在C++这个大杂烩中,可以跟很多特性结合,较为复杂,本文讲解C++中函数是如何一步步演变的。
函数
跟C中类似,将一部分代码封装起来,方便进行复用。
#include <iostream>
int add(int a, int b){ //一个最简短的函数
return a+b;
}
上述只是在进行两个int操作时的实现,如果想要再新增int和float操作呢?难道要用switch?我们可以进行函数重载:
C++中的函数重载(Function Overloading)是一种允许在相同作用域内定义多个同名函数的能力,只要这些函数的参数列表(参数的类型、数量或顺序)不同即可。函数重载是编译时多态的一种形式,它使得编译器可以根据调用时提供的参数类型和数量来确定应该调用哪个函数。
#include <iostream>
int add(int a, int b){
return a + b;
}
float add(int a, float b){
return a + b;
}
int main(){
int a = 0;
int b = 1;
float c = 2.0;
std::cout << add(a,b) << add(b,c) <<std::endl;
return 0;
C++跟C一样,为了更好利用函数,也有函数指针的概念。函数指针,本质还是指针,只不过这个指针指向一个函数。当我们想要进一步对代码进行封装,想要实现一个函数对应多个操作时,我们可以选择给函数加入一个Flag,根据flag进行switch,但是这样并不美观,且当新增操作时,需要在原函数中进行修改,使得代码不容易维护,这时我们就可以使用函数指针,为函数传入一个函数指针,该指针指向实际执行的函数。
#include <iostream>
int add(int a, int b){
return a + b;
}
int subtract(int a, int b){
return a - b;
}
enum class Operation{
ADD,
SUBTRACT
};
int my_operator1(int a, int b, Operation operation){ //不容易维护,新增加一个操作就要修改函数
switch(operation){
case Operation::ADD:
return add(a, b);
case Operation::SUBTRACT:
return subtract(a, b);
}
}
int my_operation(int a, int b, int (*operation)(int, int)){
return operation(a, b);
}
int main(){
int a = 5;
int b = 3;
int result;
int (*func_ptr)(int, int) = add;
result = my_operation(a, b, func_ptr);
std::cout << "Result: " << result << std::endl;
return 0;
}
函数模板
C++98就有的特性。上面我们只使用了int输入,如果我们想要一个函数可以接收多个输入,那就需要函数模板登场了。
#include <iostream>
template <typename T>
T add(T a, T b){
return a + b;
}
template <typename T>
T subtract(T a, T b){
return a - b;
}
template <typename T>
T my_operation(T a, T b, T (*operation)(T, T)){
return operation(a, b);
}
int main(){
int a = 5;
int b = 3;
int result;
int (*func_ptr)(int, int) = add;
result = my_operation(a, b, func_ptr);
std::cout << "Result: " << result << std::endl;
return 0;
}
如果我们又突然想要为一种输入执行特定操作怎么办呢?例如我们不想让调用者使用double类型,是不是又要switch了呢?为了解决这个问题,可以使用模板特化操作:
C++中模板特化分为全特化和偏特化,但是只有类模板才支持偏特化,函数只支持全特化。所谓偏特化,即:在有多个模板参数时,允许为一个特殊模板参数进行特殊实现,而其他参数不受限制,全特化就是指:必须指定全部模板参数为指定参数的特化。
例如:两个模板参数T和U,偏特化指可以实现一个<int, U>的特殊实现,而全特化只能为<int,int>做一个特殊实现。
#include <iostream>
template <typename T>
T add(T a, T b){
return a + b;
}
template <typename T>
T subtract(T a, T b){
return a - b;
}
template <typename T>
T my_operation(T a, T b, T (*operation)(T, T)){
return operation(a, b);
}
template<> //模板全特化
double my_operation<double>(double a, double b, double (*operation)(double, double)){
std::cout <<" suggest to use float instead of double" << std::endl;
return operation(a, b);
}
// 模板函数也可以进行重载,
//如果不加<double>,编译器会将其理解为进行了函数重载,也可以正常编译运行。
int main(){
int a = 5;
int b = 3;
int result;
int (*func_ptr)(int, int) = add;
result = my_operation(a, b, func_ptr);
std::cout << "Result: " << result << std::endl;
double c = 5.5;
double (*func_ptr2)(double, double) = add;
double result2 = my_operation(c, c, func_ptr2);
return 0;
}
模板编程在STL库中应用非常多,容器大多都使用了类模板,模板特化在STL库中也有应用,vector<bool>
就应用了模板特化,可以尝试一下,会发现sizeof(vector<T>)
输出都是24大小,但是对于vector<bool>
的结果就不一样,这是因为vector<bool>
使用了模板特化,实现跟其他的vector不一样。
#include <iostream>
#include <vector>
int main(){
std::vector<bool> test_bool(30);
std::vector<int> test_int(30);
std::vector<float> test_float(30);
std::cout << "Size of test_bool: " << sizeof(test_bool) << std::endl;
std::cout << "Size of test_int: " << sizeof(test_int) << std::endl;
std::cout << "Size of test_float: " << sizeof(test_float) << std::endl;
return 0;
}
结果:
其他的vector是24是因为vector的内部实现是三个指针,一个指向数据存储区域的开始,一个指向数据存储区域的末尾,一个指向末尾元素的下一个空位。64位电脑上,一个指针是8字节,24是三个指针的大小。
如果是我们自己调用,我们知道add函数的输入需要必须能够进行+操作,但是如果是更复杂的函数,要求输入能够进行其他更为复杂的操作呢?如何确保调用者知道需要符合什么样条件的模板参数呢?C++20引入了概念concepts特性和requires表达式,能够定义模板参数需要满足的条件。
在C++20以前,可以用SFINAE技术来实现差不多的效果,具体后续用到再学。
除了上述提到的,C++模板还有变长模板参数特性,非类型模板参数特性等等,用到再学。
仿函数
前面我们提到了C++中的函数重载,C++中还提供了操作符重载,那么我们是否可以把()操作符重载一下呢?答案是可以的,那重载了()操作符的类,是不是也能像调用函数一样,直接ClassA()
呢?答案是可以的,这就是仿函数:
C++中的仿函数(Functor)是一种特殊的对象,它允许对象模仿函数的行为。仿函数通常重载了函数调用运算符operator(),使得该对象可以像函数一样被调用。
仿函数的作用有很多:
- 可以保存状态信息:之前我们提到函数是最小的代码单元,实际上,但是之前的函数只是一段代码的复用,不包括状态信息,对于一些特殊的函数,我们需要其包括状态信息,例如深度学习中的某些算子,其中需要存储一些如阈值之类的状态信息,就可以使用仿函数来实现。
#include <iostream>
#include <vector>
#include <math.h>
class BatchNorm{
public:
BatchNorm(float epsilon=1e-5,float gamma=0.1,float beta=0.1) :
epsilon(epsilon), gamma(gamma),beta(beta)
{}
std::vector<float> operator()(std::vector<float> a);
private:
float epsilon;
float gamma;
float beta;
};
std::vector<float> BatchNorm::operator()(std::vector<float> a){
std::vector<float> result;
int sum = 0,square_sum = 0;
int mean = 0,var=0;
for(auto i : a){
sum += i;
}
mean = sum / a.size();
for(auto i : a){
square_sum += (i - mean) * (i - mean);
}
var = square_sum / a.size();
for(auto i : a){
result.push_back(beta*(i - mean) / sqrt(var + epsilon) + gamma);
}
return result;
}
int main(){
std::vector<float> a = {1,2,3,4,5};
BatchNorm batch_norm;
std::vector<float> result = batch_norm(a);
for(auto i : result){
std::cout << i << std::endl;
}
return 0;
}
lambda匿名函数
lambda匿名函数是C++11中新增的特性,其实现是一个匿名仿函数,lambda只不过是一个语法糖。
lambda语法如下:
//语法:
//[ 捕获 ] <模板形参>(可选)(C++20) ( 形参 ) 说明符(可选) 异常说明 attr -> ret requires(可选)(C++20) { 函数体 }
lambda的例子:
#include <iostream>
int main(){
int a = 5;
int b = 3;
constexpr int c = 4;
const int e = 6; //常量不需要捕获,可以直接使用
static int d = 7; //静态变量不需要捕获,可以直接使用,且可以修改
// 这种用auto的方式定义lambda表达式,是C++14的特性,被称为泛型lambda表达式
auto add = [](int a, int b) -> int{
return a + b;
};
int result = add(a, b);
std::cout << "Result: " << result << std::endl;
// 默认捕获方式是[=],表示以值的方式捕获外部变量,如果要以引用的方式捕获外部变量,可以使用[&]
// 下面等同于[&a, b],表示以引用的方式捕获a,以值的方式捕获b
[&, b]() mutable {
a++; //a是以引用的方式捕获的,所以可以修改
b++; //有了mutable关键字,就可以修改值捕获的变量,否则表达式是一个右值,无法修改
}();
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl; //值捕获的变量,即使有mutable,也只是在lambda表达式内部修改,外部不会改变
[=]() { //默认是以值的方式捕获外部变量,没有添加mutable,不能修改否则会报错
// a++;
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
}();
return 0;
}
没有任何捕获的,单纯被用作匿名函数。如果有捕获,那lambda就成了闭包,带有自己的状态信息。
之前写过一篇关于lambda和闭包的文章:Modern C++中的闭包与匿名函数