0%

深入浅出C++的function

今天我们来聊聊C++的function。std::function是从C++11开始支持的特性,它起什么作用?又有什么好处呢?

C语言中的函数指针

对C语言熟悉的同学应该都知道,C语言中有一种高级技巧叫作函数指针,我们可以让函数指针指向参数类型相同、返回值类型也相同的函数。通过函数指针我们也可以实现C++中的多态。我们来看个例子:

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

typedef int (*func)();

int print1(){
printf("hello, print1 \n");
return 0;
}

int print2(){
printf("hello, print2 \n");
return 0;
}

int main(int argc, char * argv[]){
func fp = print1;
fp();

fp = print2;
fp();

return 0;
}

上面代码中定义了一个函数指针func,它可以指向无输入参数,返回值为整型的函数。因此在main函数中,我们可以用fp(这是func类型的指针)分别指向print1和print2并调用它们。

其运行结果如下:

1
2
hello, print1
hello, print2

function的作用

从上面的C代码中我们可以看到C函数指针的作用,那在C++中是否也类似这样的功能呢?没错function就是完成这个任务的。但std::function比C的函数指针功能更强大些或者说更适合C++中使用。

下面我们来看一下如何在C++中使用std::function实现指向不同的函数吧。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void print1(){
std::cout << "hello, print1" << std::endl;
}

void print2(){
std::cout << "hello, print2" << std::endl;
}

int main(int argc, char *argv[])
{
std::function<void()> func(&print1);
func();

func = &print2;
func();

return 0;
}

上面代码与C函数指针一样定义了两个全局函数print1和print2。在main函数中又定义了std::function 对象 func,然后将print1和print2分别赋值给func,这样就可以达到与C语言中指针同样的功能了。

其运行结果如下:

1
2
hello, print1
hello, print2

可以看到std::function的结果与上面C函数指针的结果是一致的,因此std::function就是C++中用来代替C函数指针的。但如果std::function只是实现上面的功能也没啥好奇怪的对吧?实际上std::function还有一个特别有意思的用法,你可以将一个重载了()操作符的对象赋值给它,这样就可以像调用函数一样使用该对象了。下面咱们就对上面的代码做下简单修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
struct A {
void operator()() {
std::cout << "This is A Object" << std::endl;
}
};
...

int main(...){
...

A a;
func = a;
func();

...

}

在上面的代码中,使用struct定义了一个结构体,而在该结构体中重载了()操作符,因此只要你将A的类对象赋值给func,它就可以像函数一样使用了。其结果如下:

1
2
...
This is A Object

function的实现原理

是不是觉得function做的事儿还挺神奇的?它是如何实现的呢?下面我们就来扒一扒它是如何实现的。

从实现上来说,有两种办法可以实现std::function:一种是通过类的多态,即通过虚表来达到多态;另一种方法是通过C语言的函数指针来实现。今天我们只介绍通过类多态的方式来实现function,对于通过函数指针实现的方式你可以自己去研究一下。

现在我们由浅入深的来分解一下function。通过观察我们可以发现function是一个包装类,它可以接收普通函数、函数类对象(也就是实现了()操作符的类对象)等。它是如何做到的呢?

最简单的方式就是通过类模板。我们都知道function的类模板参数是可变的,但我们为了简单,所以只实现有一个参数的function类模板。这也符合我们的目标,只是扒一下实现原理,并不是想自己去实现它。

OK,下面我们来看看该如何定义这个类模板吧。

1
2
3
4
5
6
7
8
template<typename R, typename Arg0>
class myfunction<R(Arg0)> {
...
public:
R operator()(Arg0 arg0){
return ...;
}
};

上面的代码定义了一个最简单的,只有一个参数的类模板。它是 function<int(int)>function<String(int)> 等格式的类模板。这样我们在外型上与标准库中的std::function类似了。

接下来我们需要思考一下,如何让我们自己实现的function可以调用不同的函数呢?从其行为上可以推理出其内部应该有一个指针,而且这个指针具有多态性。想想C++中的多态是如何实现的?通过继承和虚表对吧。所以在function内部应该有一个基类指针,所有传入到function中的函数、类函数对象等都应该是继承于该类的子类成员。除此之外,还要在()操作符前加virtual关键字,让它创建虚表。

了解了上面的原理后,下面我们就在自己的function中增加基类及其类的指针。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename R, typename Arg0>
class myfunction<R(Arg0)> function {

private:
class __callbase {
public:
virtual R operator()(Arg0 arg0) = 0;
virtual ~__callbase() {}
};

__callbase *base_;

...

public:
...
R operator()(Arg0 arg0){
return (*__callbase)(arg0); //这里调用基类对象的()操作符
}

};

上面我们就将多态中的基类实现好了,在上面的代码中最关键是的operator()中增加了virtual关键字,这样该函数就被放到了vtable中,后面就可以在子类中实现该方法了。下面我们来实现子类。

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
...
class myfunction<R(Arg0)> function{
private:
...

template<typename F>
class __callable: public __callbase {

public:
callable(F functor)
: functor(functor){}

virtual R operator()(Arg0 arg0) {
return functor(arg0);
}

private:
F functor;
};

...
public:
...
template<typename F>
myfunction(F f): base_(new __callable<F>(f)){
}

~myfunction(){
if(base_) {
delete base_;
base_ = nullptr;
}
}
};

在子类的实现中,核心点是增加指向赋值给function类的函数指针或函数类对象,也就是上面__callable类中的F functor 成员。该成员的类型是通过模板template<typename F>推导出来的。如果我们在创建function时传入的是函数,那么functor就是一个函数指针,如果我们传入的是函数类对象,则functor就是类对象。

另外你可以发现,我分别在myfunction类的构造函数和__callable类前定义了模板F,这样当我们在main函数中创建myfunction对象时,通过类型推导就可以获到F的具体类型了。代码如下:

1
2
3
4
5
6
7
8
9
int print(int a){
...
return 0;
}

int main(...){
...
myfunction myfunc(print); //通过这句可以获得F类型为函数指针
}

有了functor成员后,还需要在构造__callable时给functor赋值,也就是让functor指向具体的函数或函数类对象。之后重载()操作符就可以直接调用具体的函数或函数类对象了。

通过以上讲解我想你应该已经知道标准库中的function实现的基本原理了。当然我们这里实现的比较简陋,真正的实现还要考虑很多性能的问题,所以实现的要比这个复杂得多。另外标准库中的实现是通过函数指针来实现的而非通过C++的多态。

不过我们今天实现的的myfunction虽然与标准库有很多不同,但原理都是类似的,对于我们理解function已经足够了。

小结

在本文中我首先向你介绍了std::function的作用以及如何使用它,之后又苞丁解牛的实现了一个最简陋的function,主要的目的是加深你对function的理解。

参考

C++高阶知识:深入分析移动构造函数及其原理
聊聊C++中的完美转发
细说智能指针
聊聊C++中的类型转换
重学C/C++中的const

欢迎关注我的其它发布渠道