先把folly/ScopeGuard.h的前118行放在这里:

  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
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

namespace folly {

class ScopeGuardImplBase {
 public:
  void dismiss() noexcept {
    dismissed_ = true;
  }

  template <typename T>
  FOLLY_ALWAYS_INLINE static void runAndWarnAboutToCrashOnException(
      T& function) noexcept {
    try {
      function();
    } catch (...) {
      warnAboutToCrash();
      std::terminate();
    }
  }

 protected:
  ScopeGuardImplBase() noexcept : dismissed_(false) {}

  static ScopeGuardImplBase makeEmptyScopeGuard() noexcept {
    return ScopeGuardImplBase{};
  }

  template <typename T>
  static const T& asConst(const T& t) noexcept {
    return t;
  }

  bool dismissed_;

 private:
  static void warnAboutToCrash() noexcept;
};

template <typename FunctionType>
class ScopeGuardImpl : public ScopeGuardImplBase {
 public:
  explicit ScopeGuardImpl(FunctionType& fn) noexcept(
      std::is_nothrow_copy_constructible<FunctionType>::value)
      : ScopeGuardImpl(
            asConst(fn),
            makeFailsafe(std::is_nothrow_copy_constructible<FunctionType>{},
                         &fn)) {}

  explicit ScopeGuardImpl(const FunctionType& fn) noexcept(
      std::is_nothrow_copy_constructible<FunctionType>::value)
      : ScopeGuardImpl(
            fn,
            makeFailsafe(std::is_nothrow_copy_constructible<FunctionType>{},
                         &fn)) {}

  explicit ScopeGuardImpl(FunctionType&& fn) noexcept(
      std::is_nothrow_move_constructible<FunctionType>::value)
      : ScopeGuardImpl(
            std::move_if_noexcept(fn),
            makeFailsafe(std::is_nothrow_move_constructible<FunctionType>{},
                         &fn)) {}

  ScopeGuardImpl(ScopeGuardImpl&& other) noexcept(
      std::is_nothrow_move_constructible<FunctionType>::value)
      : function_(std::move_if_noexcept(other.function_)) {
    // If the above line attempts a copy and the copy throws, other is
    // left owning the cleanup action and will execute it (or not) depending
    // on the value of other.dismissed_. The following lines only execute
    // if the move/copy succeeded, in which case *this assumes ownership of
    // the cleanup action and dismisses other.
    dismissed_ = other.dismissed_;
    other.dismissed_ = true;
  }

  ~ScopeGuardImpl() noexcept {
    if (!dismissed_) {
      execute();
    }
  }

 private:
  static ScopeGuardImplBase makeFailsafe(std::true_type, const void*) noexcept {
    return makeEmptyScopeGuard();
  }

  template <typename Fn>
  static auto makeFailsafe(std::false_type, Fn* fn) noexcept
      -> ScopeGuardImpl<decltype(std::ref(*fn))> {
    return ScopeGuardImpl<decltype(std::ref(*fn))>{std::ref(*fn)};
  }

  template <typename Fn>
  explicit ScopeGuardImpl(Fn&& fn, ScopeGuardImplBase&& failsafe)
      : ScopeGuardImplBase{}, function_(std::forward<Fn>(fn)) {
    failsafe.dismiss();
  }

  void* operator new(std::size_t) = delete;

  void execute() noexcept {
    runAndWarnAboutToCrashOnException(function_);
  }

  FunctionType function_;
};

template <typename FunctionType>
ScopeGuardImpl<typename std::decay<FunctionType>::type>
makeGuard(FunctionType&& fn) noexcept(
    std::is_nothrow_constructible<typename std::decay<FunctionType>::type,
                                  FunctionType>::value) {
  return ScopeGuardImpl<typename std::decay<FunctionType>::type>(
      std::forward<FunctionType>(fn));
}

ScopeGuard用于实现Go语言中defer的功能。其主要思想和ScopeLock类似,即利用C++栈展开机制——C++ runtime会对scope抛出异常之前定义的栈上对象进行逐个析构——达到出现异常时能够正常回滚的功能。scopeGuard本身定义了一个类,并在类的析构函数中注册一个回调函数,如果没有异常出现,在Guard对象销毁前,调用dismiss(),使对象销毁时不调用该函数,当异常发生时,该对象的析构函数会执行回调函数来进行某些回滚操作。

但这段代码远比想象中的复杂。因为scopeGuard本身也是一个对象,其构造函数本身也可能会出现异常,为了避免这一点,folly采用了一个很巧妙的方式,利用同样的代码给ScopeGuard的构造函数构造ScopeGuard(相当的绕啊),使得即使ScopeGuard构造失败了,依旧会调用回调函数。下面做一点分析。

首先,对ScopeGuardImpl设计了接受三种不同值的构造函数:1,非const左值,2,const左值或者右值,3,非const右值,对这些情况下的函数对象做不同处理,每次处理都会调用makeFailSafe,而makeFailSafe使用了tag-dispatch,对于函数对象的构造时是否会抛出异常做了区分,一般情况下,构造函数不抛出异常,那么调用简单版本:

1
ScopeGuardImplBase makeFailsafe(std::true_type, const void*) noexcept

简单版本仅仅构造一个empty的ScopeGuardBase基类,不做任何操作,针对构造函数抛出异常的版本,makeFailSafe会采用别的方式重新构造一个ScopeGuardImpl对象:

1
2
 auto makeFailsafe(std::false_type, Fn* fn) noexcept
      -> ScopeGuardImpl<decltype(std::ref(*fn))>

该对象如果构造失败,也会重新调用函数对象进行回滚。

绝大多数情况下调用逻辑:

1
2
3
4
5
6
makeGuard -> 
	ScopeGuardImpl(FunctionType &&fn) -> 
		(不一定调用的是非const右值版本,此处仅仅来拿举例)
		ScopeGuardImpl(Fn &&, ScopeGuardBase &&) -> 
			makeFailSafe (std::true_type version)
			构造成功		

复杂调用逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
makeGuard -> 
	ScopeGuardImpl(FunctionType &&fn) -> 
		ScopeGuardImpl(Fn &&, ScopeGuardBase &&) -> 
			makeFailSafe (std::false_type version) ->
				(因为参数是std::ref(*fn),必定是个右值) 
				ScopeGuardImpl(FunctionType &&fn) ->
					ScopeGuardImpl(Fn &&, ScopeGuardBase &&) -> 
						makeFailSafe(std::true_type version) ->
						无异常
			 makeFailSafe构造的对象调用dismiss
		构造成功

简单调用逻辑没什么好说的,主要是复杂调用逻辑,用了一个我不是很理解的方式对fn对象做了封装。为了对此进行研究,我写一个简单的functor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class functor{

public:
    functor() noexcept(false) {
    }

    functor(const functor &) noexcept(false) {

    }

    functor(functor &&) noexcept(false) {
    }

    void operator() () {
        std::cout << "exit called"<<std::endl;
    }
};

int main() {
    auto f = functor();
	auto guard = folly::makeGuard(f);
    return 0;
}

编译方式:

1
g++ scope_guard.cc -std=c++11 -g -O0 -lfolly

在functor的拷贝和移动构造函数中特别声明了该函数可能会抛出异常。通过gdb跟踪可以发现以下几个现象:

  1. f是非const左值,因此调用explicit ScopeGuardImpl(FunctionType& fn) noexcept
  2. makefailSafe调用false_type版本。
  3. 在makefailSafe中使用std::ref构造一个右值对象,std::ref(*fn)。
  4. std::ref为右值,因此调用非const右值版本的ScopeGuardImpl。
  5. std::ref的构造函数为noexcept,因此右值版本的ScopeGuardImpl调用makeFailSafe,这次会使用true_type版本,构造一个空的Base对象,这个对象仅仅是为了备用的备用。即为了上一次makeFailSafe构造出来一个Scope,因为这个不会抛出异常,因此该对象为空。
  6. 在ScopeGuard构造期间所构造的ScopeGuard的ScopeGuard(很绕),其Fn类型为std::reference_wrapper,并且其构造函数是没有异常抛出的,因此成功的绕过了functor构造时的异常检查。

尝试在functor的copy/move构造函数中抛出异常,结果并没有调用ScopeGuardImpl注册的函数,而是直接退出了。

那么在ScopeGuard中构造另一个ScopeGuard是为了什么?

答案是,你必须处理这个构造函数中抛出的异常,注册的函数才能得以执行。将代码改成如下形式:

 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
26
27
28
29
30
31
class functor{

public:
    int &_exec;

    functor(int &exec): _exec(exec) {

    }

    functor(const functor &other) noexcept(false) : _exec(other._exec) {
        std::cout<<"copy construct"<<std::endl;
        throw std::runtime_error("whoa");
    }

    void operator() () {
        std::cout<<"exit called"<<std::endl;
        _exec++;
    }
};

int main() {
    int a = 0;
    try {
        auto guard = folly::makeGuard(functor(a));
    }catch(...) {

    }

    std::cout << a << std::endl;
    return 0;
}

会发现,最终a的值变成1,并且会有exit called被打印。

如果把构造函数改成noexcept,会有什么问题? 答案是Guard不会为ScopeGuard的构造函数构造ScopeGuard,此时ScopeGuard构造时发生的异常将不会被try-catch捕捉,而程序崩溃。

##总结

  • 构造函数如果不会抛出异常,可以标记上noexcept,这样很多库代码不会为了安全生成特殊代码,能够提高程序执行效率。C++默认构造函数为noexcept(false)。

  • 为了避免在构造ScopeGuard的ScopeGuard时出现异常,folly用了一系列类型变化,

    • 首先makeGuard用universal reference接受所有的函数类型。
    • 然后利用函数重载,将函数类型分为1. 非const左值,2. const左值或右值, 3. 非const右值三种:
    1
    2
    3
    
     explicit ScopeGuardImpl(FunctionType& fn) noexcept
     explicit ScopeGuardImpl(const FunctionType& fn) noexcept
     explicit ScopeGuardImpl(FunctionType&& fn) noexcept
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    �而,需要说明的是无论fn的类型是什么,fn本身是一个左值!因此&fn是一个左值的指针,此时把传入makeFailSafe的变量指针统一为一个左值指针。
    
    最后用std::ref(*fn)这种方式,把传入的对象类型统一为一个左值引用。因此,无论你用什么方式传入对象,最后都是一个左值引用!甚至你传入一个temp的右值对象,最后也变成了一个左值引用,这样,在构造函数时,就不会出现异常!比如你可以这么玩:
    
    `cpp
    to guard = makeGuard(functor());
    `	
    
    在C++中,右值引用是一种类型,但是变量本身是左值,这很容易让人困惑,比如以下代码是成立的:
    
    `cpp
    t && a = 1;
    &a) = 2;
    = 3; 	
    `
    
    ��身是一个左值,其类型为右值引用。
    
    
  • C++允许使用const T& 指向一个const的右值。比如这么用:

    const std::string& s = "hahaha";

    首先C++会使用拷贝构造函数创建一个临时对象std::string(“hahaha”),然后让左值引用指向这个临时对象,右值。 但是不能这么用:

    std::string &s = "hahaha";

    因为临时对象是随时会消失的,最好别改。

    但是既然是const的右值,你不能改变他什么。因此不能实现move语义。这也是使用右值引用的源头,实际上右值引用的说法不准确,应该是非const右值引用。

    这里引入了一个奇怪的悖论。C++禁止对右值做左值引用,那么上面貌似用std::ref(*fn)的方式成功的将一个右值变成了左值引用?

    我尝试了以下方法,还成功了:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    auto transform(std::string && a)
    {
    	std::cout << &a << std::endl;
     	return std::ref(a);
    }
    int main() 
    {
    
     	auto a = transform("hahaha");
    	std::string &r = a.get();
    	std::cout << &r << std::endl;
     	r[0] = 'b';
    	std::cout << r << std::endl;
    }
    
    1
    2
    3
    
    �里``hahaha``明显是一个右值,但是却使用ref的方式成功变成了一个左值r。还可以对这个左值进行修改。此处存疑。
    
    
    
  • 当传入的类型非常复杂,有可能有const T&, T&, T时,可以使用std::decay去繁就简,std::decay<T>::type只是一个T。