2018年10月28日 星期日

[Effective C++] Prevent exceptions from leaving destructors



#include <random>
#include <iostream>
#include <vector>
#include <sstream>

//#define BETTER_SOLUTION
//#define OK_SOLUTION

class Resource {
public:
 Resource() : pResource(new int(count++)) {}
 ~Resource() {}

 static Resource create() {
  Resource *rsc = new Resource();
 }

 void destroy() {
  std::random_device rd;
  std::default_random_engine dre(rd());
  std::uniform_real_distribution<> urd(0, 1);

  if (urd(rd) >= 0.95) {
   std::ostringstream oStream;
   oStream << "Exception from dtor. Resource " << *pResource << " leaked.";
   throw std::runtime_error(oStream.str().c_str());
  }

  std::cout << "Release Resource " << *pResource << std::endl;
  delete pResource;
 }

 int getResource() const { return *pResource; }

private:
 int *pResource;
 static int count;
};

int Resource::count = 0;

class ResourceHolder {
public:
 ResourceHolder() {
#ifdef BETTER_SOLUTION
  released = false;
#endif
 }
 ~ResourceHolder() {
#if defined(BETTER_SOLUTION)
  if (!released) {
   try {
    // Release resource if the client didn't
    pResource.destroy();
   } catch (std::exception &e) {
    // If closing fails, log that and terminate or swallow
    std::cout << "Holder cought a failure: "<< e.what() << std::endl;
    // Optional.
    // std::abort();
   }
  }
#elif defined(OK_SOLUTION)
  try {
   pResource.destroy();
  } catch (std::exception &e) {
   // Make a log that something failed.
   std::cout << "Holder cought a failure: "<< e.what() << std::endl;

   // Optional.
   // std::abort();
  }
#else
  pResource.destroy();
#endif
 }

#ifdef BETTER_SOLUTION
 void release() {// For client code
  pResource.destroy();
  released = true;
 }
#endif

 int resource() const { return pResource.getResource(); }

 friend std::ostream& operator <<(std::ostream &, const ResourceHolder&);

private:
 Resource pResource;
#ifdef BETTER_SOLUTION
 bool released;
#endif
};

std::ostream& operator <<(std::ostream &os, const ResourceHolder& w)
{
 os << "Do something on Resource " << w.resource();

 return os;
}

void createResourceAndTestLeave()
{
 std::vector<ResourceHolder> v(50);

 for (const auto& w : v) {
  std::cout << w << std::endl;
 }

#ifdef BETTER_SOLUTION
 for (auto& w : v) {
  try {
   w.release();
  } catch (std::exception &e) {
   std::cout << "Client caught exception: " << e.what() << std::endl;
  }
 }
#endif
}

int main()
{
 createResourceAndTestLeave();
}



討論

C++ 並未限制 dtor 丟出例外,但是在 dtor 丟出例外有機會造成 resource leak (沒有接此例外的話會直接離開 dtor)。



std::vector<ResourceHolder> v(50);

Effective C++ 書上提到,dtor 在處理這50個元件時,丟出一個例外,程式會繼續跑。若再丟出一個,程式執行會終止或 undefined behavior。

實際使用 Visual Studio 2013 與 g++測試,丟出一個 exception 未接,程式就會 abort。


在討論書本上的解法前,先想想...若不管此例外,讓此例外往上 propagate,要在哪邊接此例外是一個大問題。

直覺上,考慮每次有物件被生成與銷毀後,就於使用此物件的函式/scope 外,接此例外。例如,在 createResourceAndTestLeave(); 外面加上 try&catch。使用 Visual Studio 2013 實測,第一次例外發生時,函式未等到剩餘的 ResourceHolder 銷毀就會離開,並且由 catch 接到例外。

由以上觀察,此作法實務上並不可行,一樣會 resource leak。

解法

[法一] 在 dtor 內 catch 例外,並呼叫 abort
[法二] 在 dtor 內 catch 例外,並且吞下例外
ResourceHolder::~ResourceHolder() {
 try {
  pResource.destroy();
 }
 catch (std::exception &e) {
  // Make a log that something failed.
  std::cout << "Holder cought a failure: " << e.what() << std::endl;

  // Optional.
  // std::abort();
 }
}


[法三] 提供一個介面給 client code,讓 client code 自己呼叫釋放資源的函式,並且有機會自己處理此例外。除此之外, dtor 還是會檢查 client code 有無釋放資源,若沒有,再走[法一]或[法二]
ResourceHolder::~ResourceHolder() {
 if (!released) {
  try {
   // Release resource if the client didn't
   pResource.destroy();
  }
  catch (std::exception &e) {
   // If closing fails, log that and terminate or swallow
   std::cout << "Holder cought a failure: " << e.what() << std::endl;
   // Optional.
   // std::abort();
  }
 }
}
void ResourceHolder::release() {// For client code
 pResource.destroy();
 released = true;
}





沒有留言:

張貼留言