总结
- 在成员函数内声明的 static 变量,其生命周期依旧是整个程序,与类对象的数量、构造和销毁无关;
- 对一般函数及静态成员函数而言,函数名等同于函数指针;而对非静态成员函数而言,上述不成立;
- 构造
std::thread
的背后是std::invoke()
一、成员函数中的 static 变量
这些天在工作中接触到了这样一段 Android 源码(可通过此处 查看):
1// android-10.0.0_r30, frameworks/native/libs/gui/Surface.cpp
2class FenceMonitor {
3public:
4 explicit FenceMonitor(const char* name) : /* initializer list */ {
5 std::thread thread(&FenceMonitor::loop, this);
6 //...
7 thread.detach();
8 }
9private:
10 void loop() {
11 //...
12 }
13};
14
15int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {
16 // ...
17 if (CC_UNLIKELY(atrace_is_tag_enabled(ATRACE_TAG_GRAPHICS))) {
18 static FenceMonitor gpuCompletionThread("GPU completion");
19 gpuCompletionThread.queueFence(fence);
20 }
21 // ...
22}
对于在一般函数中声明和使用 static 变量的情况,我们已经很熟悉了,但在成员函数中声明时有一个问题:对象的构造和销毁对 static 变量的存在会不会有什么影响?一番搜索之后确认,成员函数中声明的 static 变量,其行为与在一般函数中声明的一模一样,其生命周期依旧是整个程序,与对象的构造和销毁无关。举例而言:
1class Test
2{
3public:
4 void func()
5 {
6 static int count = 0;
7 ::std::cout << ++count << ::std::endl;
8 }
9};
10
11int main()
12{
13 for (int i = 0; i < 10; ++i){
14 Test t;
15 t.func();
16 }
17}
18
19// Output: 1 to 10
另外,和类的静态成员变量类似,成员函数中的 static 变量也是所有对象共享的(代码出处 ):
1class A {
2 void foo() {
3 static int i;
4 i++;
5 }
6}
7
8int main()
9{
10 A o1, o2, o3;
11 o1.foo(); // i = 1
12 o2.foo(); // i = 2
13 o3.foo(); // i = 3
14 o1.foo(); // i = 4
15}
回到上述 Android 代码,不论主程序有多少个线程创建了多少个 Surface 实例,都只会创建一个 FenceMonitor
线程,所有的 Surface 实例都只和该 FenceMonitor
线程打交道。
二、成员函数{名,指针}
我感觉上述源码中 FenceMonitor
类的写法挺有意思:把和线程相关的操作都封装在类中(构造函数创建线程;线程执行体为私有成员函数;配有相应的同步机制和对外接口),实现一个类实例对应一个线程——颇有 RAII
的感觉。于是乎想自己动手写写。写的过程中遇到一个错误:
1class ThreadObject
2{
3public:
4 explicit ThreadObject()
5 {
6 ::std::thread t(ThreadObject::defaultThreadBody, this); // Error: invalid use of non-static member function
7 t.detach();
8 }
9private:
10 void defaultThreadBody()
11 {
12 using namespace ::std::chrono_literals;
13 ::std::this_thread::sleep_for(2000ms);
14 }
15public:
16 void manipulate()
17 {
18 //...
19 }
20};
因为我记得:“函数名=函数指针”,所以就想当然地把类成员函数名传了进去。虽说这个问题很容易解决——只要在函数名前加个 &
就行——但是背后的原理还不明确:为什么“函数名=函数指针”对成员函数不成立?
详查之后,我形成了这样一种认知:成员函数指针(pointer to member function)其实属于成员指针(pointer to member),而非函数指针(pointer to function)。对函数指针而言,“函数名=函数指针”这种设计在特定场景下可以简化代码并提高可读性1;而对于成员指针来说却不存在这样的场景——成员函数指针不能作为变量被人为设定——换作更学术的话语的话,就是如 cppreference 中所说的:不能作为左值。既然场景不存在,那就没必要对其给出“函数名=函数指针”这样的设定。所以咱们还是老老实实取地址传参吧:
1::std::thread t(&ThreadObject::defaultThreadBody, this);
三、std::thread
的构造
在上述探寻的过程中我还得知,std::thread
构造函数的背后其实是 std::invoke()
函数,它会根据前二个参数的类型来使用采取不同的方式发起函数调用。具体在此处
及文档
中都有提及,可自行了解。
参考
- “函数名=函数指针”背后的 rationale:链接
- 函数名向函数指针的隐式类型转换:链接
- 对
std::invoke()
的参数(相较于 cppreference 而言)更为通俗的解释:链接