0%

CPP的std::call_once与模版编程的一个小坑

C++支持可变模版参数

1
template <typename... Args> void foo(Args... args);

std::call_once准确执行一次可调用 (Callable) 对象 f ,即使同时从多个线程调用。

有时候为了方便会把std::once_flag声明在函数内中,比如说

1
2
3
4
5
void callOne() {
static std::once_flag flag;
std::call_once(flag, []() { // do something});
});
}

但是和可变模版参数结合在一起就一些看起来神奇的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int counter{0};

template <typename... Args> void test(Args... args) {
static std::once_flag flag;
static int calls{0};
std::call_once(flag, [&]() {
std::cout << "counter:" << ++counter << " call_once_times:" << ++calls
<< std::endl;
});
}

int main() {
test();
test(1);
}

可以发现有两行输出

1
2
counter:1 call_once_times:1
counter:2 call_once_times:1

这应该是模版展开了,其实是两个不同的函数了。解决方法也很简单,把flag变量往上提即可。

问题的发现

写单例模式时,类似于这样

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
#include <mutex>
#include <utility>

template <typename T> class Singleton {
public:
template <typename... Args> static T *getInstance(Args... args);

private:
static inline T *instance{nullptr};
};

template <typename T>
template <typename... Args>
T *Singleton<T>::getInstance(Args... args) {
static std::once_flag flag;
std::call_once(flag,
[&]() { instance = new T(std::forward<Args>(args)...); });

return instance;
};

int main() {
Singleton<int>::getInstance(1);
Singleton<int>::getInstance();
}

第二次会得到一个新的实例。问题就出在static std::once_flag flag不应该在函数体中定义,应该在类中声明定义。