一、背景
有时候我们需要在第一次执行某个函数时进行一个特定的操作specifiedOperation,后面就不再执行specifiedOperation了,那么该怎么办。
二、如何解决
0)example
class Example{
public:
Example()=default;
private:
void specifiedOperation();
}
1)简单想法
一般我们会这么想:定义一个non-local的变量,比如说bool isInvoked_=false;
,然后在执行specifiedOperation()
时将该变量置为true
,函数定义大致如下:
class Example{
public:
Example()=default;
private:
void doSomething();
private:
bool isInvoked_=false;
}
void Example::doSomething(){
if(isInvoked_){
specifiedOperation();
}
isInvoked_=true;
}
这样第二次调用就不会在调用specifiedOperation()
了。
3)标准库为我们提供的方法std::call_once
1.用法
在标准库里有一个函数同样可以实现这个功能(定义于mutex),先来看一下它的签名:
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
他接受的第一个参数类型为std::once_flag
,它只用默认构造函数构造,不能拷贝不能移动,表示函数的一种内在状态。后面两个参数很好理解,第一个传入的是一个Callable,如果对于什么是Callable不了解的,可以去cppreference上查找。Callable简单来说就是可调用的东西,大家熟悉的有函数、函数对象(重载了operator()
的类)、std::function
和函数指针,C++11新标准中还有std::bind
和lambda,不熟悉的自行查阅。最后一个参数就是你要传入的参数。
在使用的时候我们只需要定义一个non-local的std::once_flag
,在调用时传入参数即可,如下所示:
class Example{
public:
Example()=default;
private:
void doSomething();
private:
std::once_flag isInvoked_;
}
void Example::doSomething(){
std::call_once(&Example::specifiedOperation,this);
}
2.内部细节
该函数传入的std::ince_flag
其实可以理解为上面我们想说的简单方法,它在调用传入的可调用对象时,如果该调用成功返回了没有抛出异常,那么他就会改变std::once_flag
对象的内部状态,下次调用std::call_once
时会首先检查std::once_flag
,如果状态已经改变了,他就不会调用传入的可调用对象。std::call_once
在签名设计时也很好地考虑到了参数传递的开销问题,可以看到,不管是Callable还是Args
,都使用了&&
作为形参。他使用了一个template中的reference fold,简单分析:
- 如果传入的是一个右值,那么
Args
将会被推断为Args
; - 如果传入的是一个const左值,那么
Args
将会被推断为const Args&
; - 如果传入的是一个non-const的左值,那么
Args
将会被推断为Args&
。
也就是说,不管你传入的参数是什么,最终到达std::call_once
内部时,都会是参数的引用(右值引用或者左值引用),所以说是零拷贝的。那么还有一步呢,我们还得把参数传到可调用对象里面执行我们要执行的函数,这一步同样做到了零拷贝,这里用到了另一个标准库的技术std::forward
,看一下它的一个签名:
template< class T >
T&& forward( typename std::remove_reference<T>::type& t ) noexcept;
参数在传递到callable对象时大概是这样的std::forward<Args>(args)
,模板参数T已经在传入参数到std::call_once
时确定了,forward做出了这样的承诺,如果传入的Args
是一个Args
,那么他将返回右值引用,如果传入的是const Args&
,那么也会返回一个const Args&
,如果传入的是Args&
,那么也会返回一个Args
,所以这些参数在传入callable是同样是一个引用,这就是所谓的perfect forward。
所以说使用std::call_once
时在参数传递方面是零拷贝的,它与std:thread
不一样,因为std::call_once
在执行函数时并没有另开线程。
其实我们在用的时候没有必要一步一步的去分析,我们只要知道,使用std::forward配合模板的reference fold,就可以实现参数传递的零拷贝。
3.注意
一旦调用的函数抛出了异常,那么下次执行std::call_once
时不会跳过,而是会再次尝试,知道函数执行成功,不抛出异常为止。
三、总结
如果你的函数操作不会产生异常,那么可以使用std::call_once
,使得代码更加的安全简洁易读。
网友评论