Strong Guarantee using Transaction
Posted on April 29th, 2008 at 23:24 by fr3@K

Abrahams Guarantees

在 C++ 的世界裡, 正確的 exception handling 是專業的 C++ programmer 不可或缺的技巧. 雖然它的概念並不困難, 但實作起來卻常不見得那麼容易.

要做到正確的 exception handling, 首先必須要了解什麼是 exception safety. 一個需要與 exception 打交道的 component 可在其介面實作人稱 Abrahams guarantees 的三種 exception safety 保證之一:

  1. The basic guarantee
  2. 允許操作失敗時改變物件的狀態, 但不能有 resource leak. 且該物件的狀態必須是可靠的仍然可以被解構, 操作失敗後該物件的狀態可以是不完全能被預測的.

  3. The strong guarantee
  4. 操作後的狀態只能是成功完成, 或是將該物件回復到操作之前的狀態並拋出一個 exception.

  5. The no-throw guarantee
  6. 操作不會拋出 exception.



接下來, 我要用下面這個 class definition 來嘗試實作上述三種 exception safety:

    Listing 1
    class contact_entry
    {
    public:
      void update_name(
        const string& first_name,
        const string& last_name);
    
      const string& get_first_name() const;
      const string& get_last_name() const;
    
      // Other member functions omitted...
    
    private:
      string first_name_;
      string last_name_;
    
      // Other member data omitted...
      // Email email_;
      // PhoneNumber phone_number_;
    };
    

先實作 basic guarantee:

    Listing 2
    void contact_entry::update_name(
      const string& first_name,
      const string& last_name)
    {
      first_name_ = first_name; // a1, may throw
      last_name_ = last_name;   // a2, may throw
    }
    

若 a1 拋出個 exception, last_name_ 還保持著操作前的狀態, 而 first_name_ 會處於一個有效但不確定的狀態.1 若 exception 是在 a2 處拋出, first_name_ 已經被修改了, 這次換到 last_name_ 處於一個有效但不確定的狀態. 這兩種狀況都沒有 resource 被 leak 掉, 因此符合 basic guarantee 的條件.2

再來實作 strong guarantee:

    Listing 3
    void contact_entry::update_name(
      const string& first_name,
      const string& last_name)
    {
      string tmp1 = first_name; // b1, may throw
      string tmp2 = last_name;  // b2, may throw
      swap(first_name_, tmp1);  // b3, no-throw
      swap(last_name_, tmp2);   // b4, no-throw
    }
    

不論 exception 在 b1 或 b2 處拋出, first_name_last_name_ 都處於原本的狀態. 若 b1 與 b2 都成功了, 只要利用不會拋出 exception 的 swapfirst_name_/last_name_tmp1/tmp2 交換所管理的資源與狀態, 同時也利用在 contact_entry::update_name 這個 function 結束時 tmp1/tmp2 自動解構的機制來釋放原本屬於 first_name_/last_name_ 的資源.

於是在任意地方失敗時, first_name_last_name_ 都將處於操作前的狀態. 若操作成功, first_name_last_name_ 都會被改變到新的狀態. 因此符合 strong guarantee 的條件.

最後是 no-throw guarantee:

    Listing 4
    void contact_entry::update_name(
      string first_name,
      string last_name)
    {
      swap(first_name_, first_name); // no-throw
      swap(last_name_, last_name);   // no-throw
    }
    

這個例子其實有偷吃步. 把 contact_entry::update_name 的 interface 改變了一點. 把原本型別為 const string& 的兩個參數改為 string. 使得我們可以在 contact_entry::update_name 的 function body 裏面直接以 swap 與 function parameter 交換資源與狀態, 而躲掉了上一個例子中可能會拋出 exception 的操作 (b1 and b2). 這樣的改變/實作是否滿足了 no-throw guarantee 的要求?

這個實作看似達成的 no-throw guarantee 是個假象. 它不過是把前面一個例子裡可能會拋出 exception 的 b1 與 b2 的操作移出 function body. 但可別忘了當 contact_entry::update_name 被呼叫, 在進入 function body 之前, 兩個型別為 string 的參數還是需要先被建構以供 function body 使用. Exception 依然可能在這時候被拋出. 所以這個例子並不符合 no-throw guarantee 的條件. 呼嚨一下倒是可以.

No-throw guarantee 嘗試失敗.3

The Strong Guarantee

從上面的解釋與例子可以看出來 basic guarantee 實在是很陽春.4 實際上只要如文中的例子一樣, 所依賴的 component 都保證了 basic guarantee (or higher), 上層的應用就自動擁有 basic guarantee.

要做到 strong guarantee 是要付出些代價的. 如第二個例子中的 temporary objects. 但只要做到 strong guarantee 就能給 user 狀態的可預測性. 一個操作不是成功就是失敗, 不會處於一個不上不下的狀態. 將具有 strong guarantee 的操作想像為 database 的 trasaction 就可以清楚知道它的好處. 麻煩的是 strong guarantee 並不總是像文中所舉的例子這麼容易實現, 例如當一個 module 的狀態完整性是基於多個成功的操作時.

想像一下如果有個 class contact_manager, 提供了管理連絡人的介面與多種查詢的方法. 為了讓這個例子簡單些, 請容許我忽略連絡人的姓氏與名字重複的問題. 它的 interface 像下面這樣:

    Listing 5
    class contact_manager
    {
    public:
      const contact_entry* find_contact_by_first_name(
        const string& first_name);
      const contact_entry* find_contact_by_last_name(
        const string& first_name);
      void add_contact(const contact_entry& contact);
    
      // Other member functions omitted...
    private:
      typedef list<contact_entry> contact_list_t;
      contact_list_t contact_list_;
    
      typedef map<string, contact_list_t::iterator> first_name_map_t;
      typedef map<string, contact_list_t::iterator> last_name_map_t;
      first_name_map_t first_name_map_;
      last_name_map_t last_name_map_;
    
      // Other member data omitted...
    };
    

連絡人都儲存在 contact_manager::contact_list_. 而 contact_manager::first_name_map_contact_manager::last_name_map_ 則是用來加速查詢. 它的實作可能是這樣:

    Listing 6
    const contact_entry* contact_manager::find_contact_by_first_name(
      const string& first_name)
    {
      contact_list_t::iterator iter = first_name_map_.find(first_name);
      if(iter == first_name_map_.end())
      {
        return NULL;
      }
      return &(*iter->second);
    }
    const contact_entry* contact_manager::find_contact_by_last_name(
      const string& last_name)
    {
      contact_list_t::iterator iter = last_name_map_.find(last_name);
      if(iter == last_name_map_.end())
      {
        return NULL;
      }
      return &(*iter->second);
    }
    void contact_manager::add_contact(const contact_entry& contact)
    {
      contact_list_t::iterator contact_iter =
        contact_list_.insert(contact_list_.end(), contact);
      first_name_map_.insert(
        make_pair(contact.get_first_name(), contact_iter));
      last_name_map_.insert(
        make_pair(contact.get_last_name(), contact_iter));
    }
    

兩個用來查詢的 member function 以一樣手法實作, 裏面的操作都是 no-throw, 也就是說它們提供的 exception safety 也都是 no-throw. 用來加入連絡人的 contact_manager::add_contact 裏面的三個 statement 雖然即便失敗也不會造成 resource leak,5 卻都可能會因為後面兩個 statement 在 memory allocation 失敗而拋出 exception 時, 導致正在被加入的連絡人無法以某 (些) 欄位查詢.6 contact_manager 的 instance 也因此喪失其狀態的完整性. 這個時候可以以巢狀 try-catch 將原本為 basic guarantee 的 exception safety 提升為 strong guarantee:

    Listing 7
    void contact_manager::add_contact(const contact_entry& contact)
    {
      contact_list_t::iterator contact_iter =
        contact_list_.insert(contact_list_.end(), contact); // d1, work1
    
      try
      {
        first_name_map_t::iterator first_name_iter =
          first_name_map_.insert(
            make_pair(contact.get_first_name(), contact_iter)); // d2, work2
    
        try
        {
          last_name_map_.insert(
            make_pair(contact.get_last_name(), contact_iter)); // d3, work3
        }
        catch(...)
        {
          first_name_map_.erase(first_name_iter); // d4, work2 rollback
          throw;
        }
      }
      catch(...)
      {
        contact_list_.erase(contact_iter); // d5, work1 rollback
        throw;
      }
    }
    

先不論巢狀 try-catch 可能造成的效能傷害. 明明是順序的 code 卻需要以巢狀的方式表現, 愈前面 (外層) 的 work (如 d1) 距離 rollback (如 d5) 愈遠, 更是讓可維護性與可讀性大大地打了一個折扣. 接下來我要提出一個作法, 可以解決前述幾個問題. 讓順序的 code 依然是順序的, 同時讓針對某 work 的 rollback 緊接在 work 的後面.

Class Transaction

下面是一個模擬 database trasaction 在失敗時會自動 rollback 的 C++ component. 介面與實作如下:

    Listing 8
    class trasaction
    {
    public:
      trasaction()
      :   rollback_needed_(false)
      {}
      ~trasaction()
      {
        if(rollback_needed_ == true)
        {
          while(rollback_stack_.empty() == false)
          {
              rollback_stack_.back()();
              rollback_stack_.pop_back();
          }
        }
      }
    
      template <
        class WorkFunctor,
        class RollbackFunctor>
      void execute(
        const WorkFunctor& work,
        const RollbackFunctor& rollback /*no-throw*/)
      {
        rollback_needed_ = true;
        rollback_stack_.push_back(rollback);
        WorkFunctor local_copy(work);
        // "operator()" is invoked via a copy of "work", so "WorkFunctor::operator()"
        // need not to be "const". i.e. relaxing the requirements on "work".
        // The other option is to follow C++ convention, passing the functor "work"
        // by value. If so, an exception may be thrown upon entering this function
        // when "work" is being copied as value, and the rollback mechanism would
        // had failed.
        local_copy();
        rollback_needed_ = false;
      }
    
      template <
        class WorkFunctor>
      void execute(
        const WorkFunctor& work)
      {
        rollback_needed_ = true;
        WorkFunctor local_copy(work);
        local_copy();
        rollback_needed_ = false;
      }
    
    private:
      typedef function<void ()> rollback_function_t;
      typedef deque<rollback_function_t> rollback_stack_t;
    
      rollback_stack_t rollback_stack_;
      bool rollback_needed_;
    };
    

搭配上 C++0x 的 lambda expressions and clousres, contact_manager::add_contact 可以以下面的方式實作, 達到 strong guarantee:

    Listing 9
    void contact_manager::add_contact(const contact_entry& contact)
    {
      contact_list_t::iterator contact_iter;
      first_name_map_t::iterator first_name_iter;
    
      // It is critical to define instances of class trasaction
      // after definitions of all references referenced in
      // lambda expressions.
      trasaction trx;
    
      trx.execute(
        // work1
        [&]()
        {
          contact_iter = contact_list_.insert(
            contact_list_.end(), contact);
        },
        // work1 rollback
        [&]()
        {
          contact_list_.erase(contact_iter);
        });
    
      trx.execute(
        // work2
        [&]()
        {
          first_name_iter = first_name_map_.insert(
            make_pair(
              contact.get_first_name(), contact_iter));
        },
        // work2 rollback
        [&]()
        {
          first_name_map_.erase(first_name_iter);
        });
      );
    
      trx.execute(
        // work3
        [&]()
        {
          last_name_map_.insert(
            make_pair(
              contact.get_last_name(), contact_iter));
        });
    }
    

這樣是不是容易閱讀與維護許多了呢?7

C++98 Unnamed Functors

只可惜 C++0x 的時代還沒到來. 如果不想手工寫那些很可能只被使用一次的 functor, 另外一個選擇是用 tr1::bind 來產生匿名 functor.8 例如將:

    Listing 10
    // work1
    [&]()
    {
      contact_iter = contact_list_.insert(
        contact_list_.end(),
        contact);
    }
    

變成:

    Listing 11
    // work1
    bind(
      &contact_list_t::insert,
      &contact_list_,
      contact_list_.end(),
      contact)
    

這個方案有兩個問題. 首先是它不能 compile. list::insert 是一個有兩個版本的 overloaded 的 member function, 一個是插入一個值, 另一個是插入一個 range.9 當 C++ 在遇到 overloaded 的 function 的時候, 沒辦法單以 function 的名稱來決議要參考的實體. 因此會有 ambiguous 的錯誤. 再來是 bind 無法如我們需要的接到 return value (contact_iter). 因此我們需要先準備幾個 forwarder function:

    Listing 12
    template <class Container>
    void insert_value_at(
      Container& cont,
      typename Container::iterator where,
      const typename Container::value_type& value,
      typename Container::iterator& ret)
    {
      ret = cont.insert(where, value);
    }
    
    template <class Container>
    void insert_value(
      Container& cont,
      const typename Container::value_type& value)
    {
      cont.insert(value);
    }
    
    template <class Container>
    void erase_at(
      Container& cont,
      typename Container::iterator where)
    {
      cont.erase(where);
    }
    

於是就可以將 contact_manager::add_contact 如此實作:

    Listing 13
    void contact_manager::add_contact(const contact_entry& contact)
    {
      contact_list_t::iterator contact_iter;
      first_name_map_t::iterator first_name_iter;
    
      // It is critical to define instances of class trasaction
      // after definitions of all references referenced in
      // bind expressions.
      trasaction trx;
    
      trx.execute(
        // work1
        bind(
          &insert_value_at,
          ref(contact_list_), // pass-by-reference
          contact_list_.end(),
          contact,
          ref(contact_iter)), // pass-by-reference
        // work1 rollback
        bind(
          &erase_at,
          ref(contact_list_), // pass-by-reference
          contact_iter)
      );
    
      trx.execute(
        // work2
        bind(
          &insert_value,
          ref(first_name_map_), // pass-by-reference
          make_pair(contact.get_first_name(), contact_iter)),
        // work2 rollback
        bind(
          &erase_at,
          ref(first_name_map_), // pass-by-reference
          first_name_iter)
      );
    
      trx.execute(
        // work3
        bind(
          &insert_value,
          ref(last_name_map_), // pass-by-reference
          make_pair(contact.get_last_name(), contact_iter))
      );
    }
    

這個版本閱讀起來比 listing 9 的實作困難. 跟 listing 7 比較起來可說是各有其優缺點.

Conclusion

IMO, 在 C++0x 的標準下使用文中的 class trasaction 以達成 strong guarantee 可大幅增加 source code 的可讀性與可維護性. 這個方式增加了 memory allocation 的動作,10 卻也免去了多重 try-catch 的 overhead. 一加一減之下整體表現還需 benchmark 後才能見真章. 好消息是 optimize 標準 container 的 memory allocation 不是一件難事, 就把這個題目留到下一次吧.

  1. 說白了就是一個除了可以被正常解構, 其他操作都沒實質意義的狀態 []
  2. stringoperator= 保障的正好也是 basic guarantee []
  3. 不論是直接或間接, 只要某 code path 有可能會失敗的 resource allocation, 最多就只能做到 strong guarantee, 達不到 no-throw guarantee []
  4. 陽春歸陽春, 總是比沒有 exception safety 來得強的多 []
  5. 因此符合 basic guarantee []
  6. contact_manager::add_contact 的 funciton body 總共有五個地方可能會拋出 exception, 你都看到了嗎? []
  7. 當然, 前題是先得把 C++0x 的 Lambda Expressions and Closures 弄熟 []
  8. 在本文的例子中, 使用 tr1::bind, Boost.Bind 或 Boost.Lambda 的結果是完全一樣的 []
  9. 所有標準 container 的 insert 與 erase 都有 overloaded 的設計 []
  10. transaction::performrollback 推進 transaction::rollback_stack_ 時 []
del.icio.us:Strong Guarantee using Transaction digg:Strong Guarantee using Transaction spurl:Strong Guarantee using Transaction newsvine:Strong Guarantee using Transaction furl:Strong Guarantee using Transaction Y!:Strong Guarantee using Transaction 黑米共享書籤:Strong Guarantee using Transaction 推推王:Strong Guarantee using Transaction
Previous Post
« re: Socket Programming in C 常犯的錯誤 «
Next Post
» Richard Stallman 來台演講 »

Zero Comments »

No comments yet.

Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>