从一段 Android 源码说开去

总结

  • 成员函数内声明的 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 而言)更为通俗的解释:链接

  1. 比如这里 提到,对于那种带有一堆函数指针的结构体,“函数名=函数指针”的设计可以让代码写成 graphics.open(file) 而非 (*graphics.open)(file)。 ↩︎


【前缀+Hash】与【双指针】的区别
并查集与搜索的联系