std::call_once的使用

2019-03-16  本文已影响0人  琼蘂无徵朝霞难挹

一、背景

有时候我们需要在第一次执行某个函数时进行一个特定的操作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,简单分析:

  1. 如果传入的是一个右值,那么Args将会被推断为Args
  2. 如果传入的是一个const左值,那么Args将会被推断为const Args&
  3. 如果传入的是一个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,使得代码更加的安全简洁易读。

上一篇 下一篇

猜你喜欢

热点阅读