auto_ptr原始碼分析

FreeeLinux發表於2017-01-18

By SmartPtr(http://www.cppblog.com/SmartPtr/)

auto_ptr是當前C++標準庫中提供的一種智慧指標,或許相對於boost庫提供的一系列眼花繚亂的智慧指標, 或許相對於Loki中那個無所不包的智慧指標,這個不怎麼智慧的智慧指標難免會黯然失色。誠然,auto_ptr有這樣那樣的不如人意,以至於程式設計師必須像使用”裸“指標那樣非常小心的使用它才能保證不出錯,以至於它甚至無法適用於同是標準庫中的那麼多的容器和一些演算法,但即使如此,我們仍然不能否認這個小小的auto_ptr所蘊含的價值與理念。
  auto_ptr的出現,主要是為了解決“被異常丟擲時發生資源洩漏”的問題。即如果我們讓資源在區域性物件構造時分配,在區域性物件析構時釋放。這樣即使在函式執行過程時發生異常退出,也會因為異常能保證區域性物件被析構從而保證資源被釋放。auto_ptr就是基於這個理念而設計, 這最早出現在C++之父Bjarne Stroustrup的兩本鉅著TC++PL和D&E中,其主題為”resource acquisition is initialization”(raii,資源獲取即初始化),然後又在Scott Meyer的書推動下,被加入了C++標準庫。
  下面我就列出auto_ptr的原始碼,並詳細講解每一部分。因為標準庫中的程式碼要考慮不同編譯器支援標準的不同而插入了不少預編譯判斷,而且命名可讀性不是很強(即使是侯捷老師推薦的SGI版本的stl,可讀性也不盡如人意), 這裡我用了Nicolai M. Josuttis(<

namespace std
{
 template<class T>
 class auto_ptr 
 {
 private:
  T* ap; 
 public:

  // constructor & destructor ----------------------------------- (1)
  explicit auto_ptr (T* ptr = 0) throw() : ap(ptr){}

  ~auto_ptr() throw() 
  {
   delete ap;
  }


  // Copy & assignment --------------------------------------------(2)
  auto_ptr (auto_ptr& rhs) throw() :ap(rhs.release()) {}
  template<class Y>  
  auto_ptr (auto_ptr<Y>& rhs) throw() : ap(rhs.release()) { }

  auto_ptr& operator= (auto_ptr& rhs) throw() 
  {
   reset(rhs.release());
   return *this;
  }
  template<class Y>
  auto_ptr& operator= (auto_ptr<Y>& rhs) throw() 
  {
   reset(rhs.release());
   return *this;
  }

  // Dereference----------------------------------------------------(3)
  T& operator*() const throw() 
  {
   return *ap;
  }
  T* operator->() const throw() 
  {
   return ap;
  }

  // Helper functions------------------------------------------------(4)
  // value access
  T* get() const throw() 
  {
   return ap;
  }

  // release ownership
  T* release() throw()
  {
   T* tmp(ap);
   ap = 0;
   return tmp;
  }

  // reset value
  void reset (T* ptr=0) throw() 
  {
   if (ap != ptr) 
   {
    delete ap;
    ap = ptr;
   }
  }

  // Special conversions-----------------------------------------------(5)
  template<class Y>
  struct auto_ptr_ref
  {
   Y* yp;
   auto_ptr_ref (Y* rhs) : yp(rhs) {}
  };

  auto_ptr(auto_ptr_ref<T> rhs) throw() : ap(rhs.yp) { }
  auto_ptr& operator= (auto_ptr_ref<T> rhs) throw() 
  {  
   reset(rhs.yp);
   return *this;
  }
  template<class Y>
  operator auto_ptr_ref<Y>() throw() 
  {
   return auto_ptr_ref<Y>(release());
  }
  template<class Y>
  operator auto_ptr<Y>() throw()
  {
   return auto_ptr<Y>(release());
  }
 };
}

1 建構函式與解構函式
auto_ptr在構造時獲取對某個物件的所有權(ownership),在析構時釋放該物件。我們可以這樣使用auto_ptr來提高程式碼安全性:
int* p = new int(0);
auto_ptr ap(p);
從此我們不必關心應該何時釋放p, 也不用擔心發生異常會有記憶體洩漏。
這裡我們有幾點要注意:
1) 因為auto_ptr析構的時候肯定會刪除他所擁有的那個物件,所有我們就要注意了,一個蘿蔔一個坑,兩個auto_ptr不能同時擁有同一個物件。像這樣:
int* p = new int(0);
auto_ptr ap1(p);
auto_ptr ap2(p);
因為ap1與ap2都認為指標p是歸它管的,在析構時都試圖刪除p, 兩次刪除同一個物件的行為在C++標準中是未定義的。所以我們必須防止這樣使用auto_ptr.
2) 考慮下面這種用法:
int* pa = new int[10];
auto_ptr ap(pa);
因為auto_ptr的解構函式中刪除指標用的是delete,而不是delete [],所以我們不應該用auto_ptr來管理一個陣列指標。
3) 建構函式的explicit關鍵詞有效阻止從一個“裸”指標隱式轉換成auto_ptr型別。
4) 因為C++保證刪除一個空指標是安全的, 所以我們沒有必要把解構函式寫成:
~auto_ptr() throw()
{
if(ap) delete ap;
}

2 拷貝構造與賦值
與引用計數型智慧指標不同的,auto_ptr要求其對“裸”指標的完全佔有性。也就是說一個”裸“指標不能同時被兩個以上的auto_ptr所擁有。那麼,在拷貝構造或賦值操作時,我們必須作特殊的處理來保證這個特性。auto_ptr的做法是“所有權轉移”,即拷貝或賦值的源物件將失去對“裸”指標的所有權,所以,與一般拷貝建構函式,賦值函式不同, auto_ptr的拷貝建構函式,賦值函式的引數為引用而不是常引用(const reference).當然,一個auto_ptr也不能同時擁有兩個以上的“裸”指標,所以,拷貝或賦值的目標物件將先釋放其原來所擁有的物件。
這裡的注意點是:
1) 因為一個auto_ptr被拷貝或被賦值後, 其已經失去對原物件的所有權,這個時候,對這個auto_ptr的提領(dereference)操作是不安全的。如下:

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2 = ap1;
cout<<*ap1; //錯誤,此時ap1只剩一個null指標在手了

這種情況較為隱蔽的情形出現在將auto_ptr作為函式引數按值傳遞,因為在函式呼叫過程中在函式的作用域中會產生一個區域性物件來接收傳入的auto_ptr(拷貝構造),這樣,傳入的實參auto_ptr就失去了其對原物件的所有權,而該物件會在函式退出時被區域性auto_ptr刪除。如下:

void f(auto_ptr<int> ap){cout<<*ap;}
auto_ptr<int> ap1(new int(0));
f(ap1);
cout<<*ap1; //錯誤,經過f(ap1)函式呼叫,ap1已經不再擁有任何物件了。

因為這種情況太隱蔽,太容易出錯了, 所以auto_ptr作為函式引數按值傳遞是一定要避免的。或許大家會想到用auto_ptr的指標或引用作為函式引數或許可以,但是仔細想想,我們並不知道在函式中對傳入的auto_ptr做了什麼, 如果當中某些操作使其失去了對物件的所有權, 那麼這還是可能會導致致命的執行期錯誤。 也許,用const reference的形式來傳遞auto_ptr會是一個不錯的選擇。

2)我們可以看到拷貝建構函式與賦值函式都提供了一個成員模板在不覆蓋“正統”版本的情況下實現auto_ptr的隱式轉換。如我們有以下兩個類

class base{};
class derived: public base{};
//那麼下列程式碼就可以通過,實現從auto_ptr<derived>到auto_ptr<base>的隱式轉換,因為derived*可以轉換成base*型別
auto_ptr<base> apbase = auto_ptr<derived>(new derived);

3) 因為auto_ptr不具有值語義(value semantic), 所以auto_ptr不能被用在stl標準容器中。
所謂值語義,是指符合以下條件的型別(假設有類A):

A a1;
A a2(a1);
A a3;
a3 = a1;
那麼
a2 == a1, a3 == a1

很明顯,auto_ptr不符合上述條件,而我們知道stl標準容器要用到大量的拷貝賦值操作,並且假設其操作的型別必須符合以上條件。

3 提領操作(dereference)
提領操作有兩個操作, 一個是返回其所擁有的物件的引用, 另一個是則實現了通過auto_ptr呼叫其所擁有的物件的成員。如:

struct A
{ 
 void f();
}
auto_ptr<A>  apa  (new A);  
(*apa).f();
apa->f();

當然, 我們首先要確保這個智慧指標確實擁有某個物件,否則,這個操作的行為即對空指標的提領是未定義的。

4 輔助函式
1) get用來顯式的返回auto_ptr所擁有的物件指標。我們可以發現,標準庫提供的auto_ptr既不提供從“裸”指標到auto_ptr的隱式轉換(建構函式為explicit),也不提供從auto_ptr到“裸”指標的隱式轉換,從使用上來講可能不那麼的靈活, 考慮到其所帶來的安全性還是值得的。
2) release,用來轉移所有權
3) reset,用來接收所有權,如果接收所有權的auto_ptr如果已經擁有某物件, 必須先釋放該物件。

5 特殊轉換
這裡提供一個輔助類auto_ptr_ref來做特殊的轉換,按照標準的解釋, 這個類及下面4個函式的作用是:使我們得以拷貝和賦值non-const auto_ptrs, 卻不能拷貝和賦值const auto_ptrs. 我無法非常準確的理解這兩句話的意義,但根據我們觀察與試驗,應該可以這樣去理解:沒有這些程式碼,我們本來就可以拷貝和賦值non-const的auto_ptr和禁止拷貝和賦值const的auto_ptr的功能, 只是無法拷貝和賦值臨時的auto_ptr(右值), 而這些輔助程式碼提供某些轉換,使我們可以拷貝和賦值臨時的auto_ptr,但並沒有使const的auto_ptr也能被拷貝和賦值。如下:
auto_ptr ap1 = auto_ptr(new int(0));
auto_ptr(new int(0))是一個臨時物件,一個右值,一般的拷貝建構函式當然能拷貝右值,因為其引數類別必須為一個const reference, 但是我們知道,auto_ptr的拷貝函式其引數型別為reference,所以,為了使這行程式碼能通過,我們引入auto_ptr_ref來實現從右值向左值的轉換。其過程為:
1) ap1要通過拷貝 auto_ptr(new int(0))來構造自己
2) auto_ptr(new int(0))作為右值與現有的兩個拷貝建構函式引數型別都無法匹配,也無法轉換成該種引數型別
3) 發現輔助的拷貝建構函式auto_ptr(auto_ptr_ref rhs) throw()
4) 試圖將auto_ptr(new int(0))轉換成auto_ptr_ref
5) 發現型別轉換函式operator auto_ptr_ref() throw(),轉換成功,從而拷貝成功。
從而通過一個間接類成功的實現了拷貝構造右值(臨時物件)
同時,這個輔助方法不會使const auto_ptr被拷貝,原因是在第5步,此型別轉換函式為non-const的,我們知道,const物件是無法呼叫non-const成員的, 所以轉換失敗。當然,這裡有一個問題要注意,假設你把這些輔助轉換的程式碼註釋掉,該行程式碼還是可能成功編譯,這是為什麼呢?debug一下,我們可以發現只呼叫了一次建構函式,而拷貝建構函式並沒有被呼叫,原因在於編譯器將程式碼優化掉了。這種型別優化叫做returned value optimization,它可以有效防止一些無意義的臨時物件的構造。當然,前提是你的編譯器要支援returned value optimization。

可見,auto_ptr短短百來行的程式碼,還是包含了不少”玄機”的。

相關文章