2016年3月18日 星期五

無異常(non-exception)之下的RAII

作者: Eli Bendersky
原文網址: http://eli.thegreenplace.net/2016/c-raii-without-exceptions/

我已讀遍了"RAII只能用在異常(exception)機制上"相關琳瑯滿目的文章,但我忍受不了了


TL; DR 這篇文章並不探討異常機制(exception)到底是好還是壞。而是關於RAII這類的動態資源管理技術在有無異常機制底下,都能做出有用的自我管理。尤其我想解釋在你的C++程式碼中即使關閉了異常機制,為何RAII還是很有用


基礎
我們寫個RAII能包裹住 FILE*並自動做出關閉檔案的handle
class FileHandle {
  public:
    FileHandle(const char* name, const char* mode) {
      f_ = fopen(name, mode);
    }

    FILE* file() {
      return f_;
    }

    ~FileHandle() {
      if (f_ != nullptr) {
        fclose(f_);
      }
    }

  private:
    FILE* f_;
};
我們有個範例能利用它

std::string do_stuff_with_file(std::string filename) {
  FileHandle handle(filename.c_str(), "r");
  int firstchar = fgetc(handle.file());

  if (firstchar != '$') {
    return "bad bad bad";
  }

  return std::string(1, firstchar);
}
請記得這裡沒有異常機制了-已事先使用-fno-exceptions 且無try敘述
FileHandle這類RAII仍很重要do_stuff_with_file有兩個脫離點(譯可立即離開函式的觸發點),此檔案能在任一點關閉起來do_stuff_with_file是個又短又簡單的函式。在大型函式中會有很多個脫離點,這使得管理資源的釋放容易出錯,RAII技術在此變得極為重要

RAII本質上用堆疊配置物件(譯區域物件也是此類物件)的建構式來獲取一些資源,並由解構式來釋放資源。不論引發異常或是函式返回的情況下編譯器保證離開這類物件的作用域時物件的解構式會做出正確的資源釋放

RAII並不是指你得在建構式中配置或增加任何東西。而是指操控邏輯執行」所需的"鬆綁"參考計數器(reference counting)是個好例子。很多資料庫跟軟體庫擁有"游標"這類抽象來藉此提供資料存取。這裡我們做出如何安全地前進(increase)和退回(decrease )游標的參考計數

class CursorGuard {
public:
  CursorGuard(Cursor* cursor) : cursor_(cursor) {
    cursor_->incref();
  }

  Cursor* cursor() {
    return cursor_;
  }

  ~CursorGuard() {
    cursor_->decref();
  }

private:
  Cursor* cursor_;
};


void work_with_cursor(Cursor* cursor) {
  CursorGuard cursor_guard(cursor);

  if (cursor_guard.cursor()->do_stuff()) {
    // ... do something
    return;
  }

  // ... do something else
  return;
}
再一次地RAII確保了work_with_cursor不會讓游標的參考計數器出現脫軌的情況:一旦前進(incref( ) )後,無論函式從哪意外返回,保證一定會有退回(decref( ))動作

標準程式庫裡的RAII
像"哨兵"一樣的RAII classes是極普遍且實用的技術,也紮根在標準程式庫中。如C++11執行緒程式庫針對mutex鎖定而設計的lock_guard,如下例子
void safe_data_munge(std::mutex& shared_mutex, Data* shared_data) {
  std::lock_guard<std::mutex> lock(shared_mutex);
  shared_data->munge();

  if (...) {
    shared_data();
    return;
  }

  shared_data->munge_less();
  return;
}
std::lock_guard的建構式鎖定mutex,然後在解構式中解鎖mutex,確保在safe_data_munge裡能從頭到尾保護好共享資料的存取完整性,真正的解鎖動作也總是確實執行

RAII與C++11
有關係到標準程式庫的話,我不得不提最具RAII特色的型別std::unique_ptr。在C和C++中資源管理(Resource management)是個龐大複雜的課題在C++普遍常見的資源管理都跟堆積(heap)記憶體有關。前面提到的C++11中,有很多靈活指標(smart pointers)這類的第三方解決方案,加上C++11搬移語意(move semantics)最後讓語言有了以RAII方式所創造出堅固的靈活指標

void using_big_data() {
  std::unique_ptr<VeryVeryBigData> data(new VeryVeryBigData);

  data->do_stuff();

  if (data->do_other_stuff(42)) {
    return;
  }

  data->do_stuff();
  return;
}
我們無論怎樣使用data,無論怎樣返回函式,所配置的記憶體都將釋放回去。假如你的編譯器支援C++14,在創造指標那行使用std::make_unique會變得更簡潔

// Good usage of 'auto': removes the need to repeat a (potentially long)
// type name, and the actual type assigned to 'data' is trivially obvious.
auto data = std::make_unique<VeryVeryBigData>();
std::unique_ptr功能多且還有其他應用 ,但這裡我只關注堆積記憶體上的RAII"如值(value)一般"的處理方式
這裡強調C++11為何對RAII而言如此的重要:假如沒有搬移語意會導致copy語意跟可觀的開銷那我們只能為"靈活"的指標而寫出令人瞠目結舌的東西。有了搬移語意,就能從函式中輕易"轉移物件的擁有權"到其他地方而不需要多大的開銷。過去以來C++程式員時常為了從程式碼中擠出一些效能,大部份都選擇讓自己在艱困的環境中處理原始(raw)指標。使用C++11和std::unique_ptr的話,會得到更有效率的搬移且無需額外記憶體佔用,使得這問題不太需顧慮嚴謹跟安全,不用得付出效能的代價

其他語言的RAII理念
一個常被問到的C++問題:"為何C++不能像其他語言(Java,C#,Python)一樣享有finally建構方式呢?"Stroustrup給的答案RAII是更好的替代方案Stroustrup認為(怒我直言這就是現實) 現實程式碼中獲取跟釋放資源的出現次數遠高於明確區別各類資源,所以RAII就使程式碼更簡短了。除此之外,你一旦用RAII把程式碼包裹住後,將不容易出錯,也不需手動釋放資源。下面的work_with_cursor以假設的finally方式來重寫前面提過的例子(這不是真實的C++程式碼)

// Warning: this is not real C++
void work_with_cursor(Cursor* cursor) {
  try {
    cursor->incref();

    if (cursor->do_stuff()) {
      // ... do something
      return;
    }

    // ... do something else
    return;
  }
  finally {
    cursor->decref();
  }
}
是的,它使得程式碼變得多餘。較重要是必須記得使用cursor->decref()。大型程式不斷改變資源的取捨,在實踐上你將用try...finally區塊來當作每個涵式本體的骨架,然後得記住要做出釋放資源的動作。假使是用前面的CursorGuard來當幫手的話,只需定義一次哨兵class(CursorGuard)就能省下先前所付出的全部開銷

另一個好例子是在Python語言。即使Python有finally的建構方式現代的Python程式碼則偏好用途更多廣的with敘述。with支援 "context managers",非常類似C++的RAII。with比finally好用且更具通用性,這就是為何你在程式碼中能大量看到它們的身影

那RAII如果是在異常機制底下呢?
講到這即使是在非異常機制下,我希望你已信服RAII在C++中是重要且實用的技術
RAII跟異常之間的親密關係已經受到人們的認可,不管怎樣,因為撰寫異常安全性(exception-safe)程式碼卻不用RAII是幾乎不可能地。在啟用異常機制下,我們不會只檢查函式中每個return敘述來診斷出何處會導致資源洩漏。每一行程式碼都可能藏有地雷。函式或方法的呼叫處呢?也是會丟出異常。在堆疊區建構非POD物件呢?也是會丟出異常。copy物件到別處呢?也是一樣...丟出異常a + b?可藉由+operator丟出異常

另外異常跟建構式中的RAII也有緊密的關係。由於建構式無法返回數值(value)因此,建構式遇到錯誤的情況下,你可以丟出異常或製造物件內部的錯誤狀態。後者有它自身的問題(非異常機制的程式碼中使用此建構方法是較好的選擇),所以丟出異常是最常見的手段。自此RAII對異常機制是如此重要,也是因為RAII跟建構式常常相伴而行(記住-RAII在物件被建構時就開始了),這種緊密的關係就烙印在C++學習者心中

RAII不只在異常下有用處。它更在C++資源管理技術中扮演重要角色。因此,去假設有RAII就表示某種程度上你的程式碼是把異常(exception)給泛濫亂用的地方,這是無意義的。甚至真的使用了異常也是如此。批判C++的異常安全性是情有可原,但批判RAII就說不過去了,因為RAII只是種解決手段,而非問題的源頭所在

最後,在我個人筆記將加上,我並不是C++的異常機制的粉絲,而是RAII的大粉絲。當我用C++時,我寧可不用異常機制,或至少把它給約束和限制在程式裡的極小區域內。可用RAII的話,我會採用標準庫中的class像std::unique_ptr或者是自己撰寫的RAII程式碼。RAII在我心目中是優良且具實用性的C++特色之一,幫助維持大型程式碼的穩健跟安全

2016年3月9日 星期三

認識SFINAE(補充)

作者:Bartlomiej Filipek
原文出處:http://www.bfilipek.com/2016/02/sfinae-followup.html


SFINAE Followup



我上一篇發的SFINAE的反應似乎不錯!我收到非常寶貴的評論跟建議。於是動筆寫這篇來做為補充。

運用現代C++表達
在其中意見回覆中STL (Stephan T. Lavavej) 說我在文章中的解決方案是舊式的C++風格。那最新現代的風格會有什麼效果呢?

decltype

decltype 推斷出運算式(expression)的返回型別的強大工具我已經使用過它了:
template <typename C> 
static YesType& test( decltype(&C::ToString) ) ;

decltype會返回C::ToString成員方法的型別(如果類別內存在這方法的話)。

declval

declval 的效用是讓你真的建構一個物件就能呼叫T裡面的方法。在之前的例子中我們能用它來推斷方法的返回型別:
decltype(declval<T>().toString())

constexpr

constexpr 會建議編譯器在編譯期就算出運算式的結果(如果可能的話)沒有它的話可能得在執行期檢驗我們的成員方法了。所以新的風格建議對大多數的methods都增加constexpr


void_t

言講影片:


從29分開始,尤其39分段落

這是令人驚奇的超編程(meta-programming)典範!我不想去講解它,您只需看影片就能理解void_t的概念思維! :)

detection 慣用法

Walter E. Brown計劃一個具完全通用的class,能用來檢測介面跟其它class屬性。當然,大部份是以void_t技術為基礎來實現


檢驗返回型別
上一次我丟出未解問題:如何約束ToString()方法的返回型別。我之前的原始碼能檢驗方法的名稱,但無法檢驗它的返回型別
Björn Fahller 給了我以下的回答: (在文章下面的評論)
template <typename T>
class has_string{
  template <typename U>
  static constexpr std::false_type test(...) { return {};}
  template <typename U>
  static constexpr auto test(U* u) ->
    typename std::is_same<std::string, decltype(u->to_string())>::type { return {}; }
public:
  static constexpr bool value = test<T>(nullptr);
};

class with_string {
public:
  std::string to_string();
};

class wrong_string{
public:
  const char* to_string();
};

int main() {
  std::cout
    << has_string<int>::value
    << has_string<with_string>::value
    << has_string<wrong_string>::value << '\n';
}

此程式將輸出:

010

在test方法中檢驗to_string()返回型別是否為我們渴望的std::string()。這class包含兩個層級的測試 : 先使用SFINAE -檢驗class有無to_string方法(若無,我們則退回採用test(...))。在這之後,檢驗返回型別是不是我們想要的。最終當我們傳入錯誤的class或class裡的to_string()返回錯誤的型別時,都會從has_string<T>::value中獲得false。這範例太棒了!

請注意constexpr被放置在::value跟test的前面,所以我們明確運用了現代C++表達方式


更多範例

指標轉換 :

Andre Weissflog的twitter:
@fenbf 我找到能透過pointee-type來使smart指標進行有用的靜態轉換template 定義式雖然醜陋,但它能順利執行篩選任務:
https://github.com/floooh/oryol/blob/master/code/Modules/Core/Ptr.h#L137 

讓我們看看程式碼:
 /// cast to compatible type
template<class U, 
    class=typename std::enable_if<std::is_convertible<T*,U*>::value>::type>
    operator const Ptr<U>&() const 
    {
        return *(const Ptr<U>*)this;
    };

這是 Ptr.h - smart pointer class 裡某一部份的程式碼,這class是在 oryol - Experimental C++11 multi-platform 3D engine 裡面。

它可能很難讀懂,但我們試著理解看看:
最關鍵的部份是 std::is_convertible<T*,U*>(請看std::is_convertible 介紹)。這個組件被裝載到if_enable運算式組件中。基本上,當T*能被轉換到U*時,我們就能獲得有效用的函式重載。否則編譯器將會對此做出報怨。

還有其它的範例嗎? 讓我欣賞下! :)


更新的版本
我假設確定你的編譯器/程式庫有支援void_t,新的程式碼如下:
// default template:
template< class , class = void >
struct has_toString : false_type { };

// specialized as has_member< T , void > or sfinae
template< class T >
struct has_toString< T , void_t<decltype(&T::toString) > > : std::is_same<std::string, decltype(declval<T>().toString())>
{ };

http://melpon.org/wandbox/permlink/ZzSz25GJVaY4cvzw

相當不錯...對吧? :)

它使用了以detection慣用法為基礎的void_t。基本上,在class內無T::toString()時,SFINAE將會啟用且我們用泛化方式來做出結果,也就是default Template(繼承自false_type)。但是當toString()存在於class內時特化(specialized)本的Template將被選上。我們不在乎toString方法的返回型別的話,這檢驗應該算結束了。但這版本我們還能繼續檢驗用來繼承的is_same。就能檢驗出方法的返回型別是否為std::string。然後我們就能以 true_type 或false_type方式來做為結果

總結
再次感謝大家的意見回饋。此文貼上來後,SFINAE/Templates使我更加混淆且對它們更一知半解:) 但仍值得去嘗試了解此機制的背後運作原理