effective C++(五)

智能指針是行爲像指針的對象,而真實指針做的很好的一件事是,支持隱式轉換,派生類指針可以轉換爲基類指針等

在同一個template的不同具現體之間並不存在什麼與生俱來的固有關係,如果以帶有base-derived關係的B,D兩類型分別具現化某個template,產生出來的兩個具現體並不帶有base-derived關係,因此它們之間並不能相互轉換,要讓它們之間能轉換我們必須將它們的轉換函數明確地編寫出來

template<typename T>
class SmartPtr{
 public:
 template<typename U>
 SmartPrt(const SmartPtr<U>& other)
  :heldPtr(other.get()){...}
 T* get() const{ return heldPtr;}
 ...
 private:
 T* heldPtr;
};

以上代碼的意思是,對任何類型T和任何類型U,這裏可以根據SmartPtr<U>生成一個SmartPtr<T>,這個構造函數根據對象U創建對象T,而U和v的類型是同一個template的不同具現體,有時我們稱之爲泛化複製構造函數,上面的泛化構造函數沒有被聲明爲explicit,因爲原始指針類型之間的轉換時隱式的,之所以需要一個T類型的指針是因爲讓這個行爲只有當存在某個隱式轉換可將一個U*指針轉爲一個T*指針時才能通過編譯,也即更像真實指針只有當轉換合理時才允許轉換。

在class內聲明泛化複製構造函數並不會阻止編譯器生成它們自己的複製構造函數。

模板化的類和非模板化的類有所不同

template<typename T>
class Rational{...}
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{...}
Rational<int> oneHalf(1,2);
Rational<int>result=oneHalf*2;      //無法通過編譯

或許我們期望編譯器使用構造函數將2轉換爲Rational<int>,進而將T推導爲int,但它們不那麼做,因爲在template實參推導過程中從不講隱式類型轉換函數納入考慮,因爲在能夠調用一個函數之前,首先必須知道那個函數存在,而爲了知道它,必須先爲相關的function template推導出參數類型,然後纔可將適當的函數具現化出來。解決方式是聲明爲firend函數

template<typename T>
class Rational{
 public:
 ...
 friend const Rational operator* (const Rational& lhs,const Rational& rhs); //在一個類模板內,template名稱可被用來作爲template和其參數的
};                                                                              簡略表達方式,所以在Rational<T>內我們可以只寫Rational
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{...}

爲了將inline聲明所帶來的衝擊最小化,做法是令operator*不做任何事情,只調用一個定義於class外部的輔助函數

template<typename T> const Rational<T> domultiply(const Rational<T>& lhs,const Rational<T>& rhs);
....
friend const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{ return domultiply(lhs,rhs);}

當編寫類模板,而它所提供之與此模板相關的函數支持所有參數之隱式類型轉換時,將那些函數定義爲類模板內部的friend函數

input迭代器只能向前移動,一次一步,客戶只能讀取且只能讀取一次,它們模仿指向輸入文件的閱讀指針,代表爲istream_iteraotrs,out迭代器情況類似,模仿指向輸出文件的塗寫指針,ostream_iterators是代表,forward迭代器可以做前兩種分類所能做的每一件事,而且可以讀或寫所指物一次以上,Bidirectional迭代器比上一個分類威力更大,除了可以向前移動,還可以向後移動,stl的list迭代器就屬於這一分類,之後最強大的就是隨機訪問迭代器了。對它們,c++標準庫分別提供專屬的卷標結構(tag struct)加以確認:

struct input_iterator_tag{};
sturct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public input_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};

這些structs之間的繼承關係是有效的is-a關係。所有forward迭代器都是Input迭代器等。

traits,它允許你在編譯期間取得某些類型信息,Traits並不是C++關鍵字或一個預先定義好的構件,它們是一種技術,也是一個C++程序員共同遵守的協議,類型的traits信息必須位於類型自身外,標準技術是把它放進一個template及其一或多個特化版本中,這樣的template在標準程序庫中有若干個,其中針對迭代器者被命令爲iterator_traits;

template<typename IterT>
struct iterator_traits;    //習慣上,traits總是被實現爲structs
template<..>
class deque{
 public:
 class iterator{
 public typedef random_access_iterator iterator_category;//iterator_traits的運作方式是,針對每一個類型IterT,在內聲明一個typedef名爲
 ...                                                        iterator_category用來確認適當IterT的迭代器分類
};
template<...>
class list{
 public:
 class iterator{
 public:
 typedef bidirectional_iterator_tag iterator_category;    //類型IterT的iterator_category其實就是用來表現IterT說它自己是什麼
 ...
};

爲了支持指針迭代器,我們需要特別針對指針類型提供一個偏特化版本

template<typename IterT>
struct iterator_traits<IterT*>
{
 typedef random_access_iterator_tag iterator_category;
};

iterator_traits<IterT>::iterator_category可在編譯期間確定,但是if語句是在運行期纔會覈定,這不僅浪費時間,也造成可執行文件膨脹,如果想判斷編譯器覈定成功類型,可以使用重載

template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::random_access_iterator_tag)
{
 iter+=d;
}
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::bidirectional_iterator_tag)
{
 if(d>=0){while(d--) ++iter;}
 else {while (d++) --iter;}
}
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::input_iterator_tag)
{
 if(d<0){
 throw std::out_of_range("Negative distance");}
 while(d--) ++iter;
}
template<typename IterT,typename DistT>
void advance(Iter& iter,DistT d)
{
 doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
};

使用traits class,建立一組重載函數或函數模板,彼此之間的差異只在於各自的traits參數,令每個函數實現碼與其接受之traits信息相應和,建立一個控制函數或函數模板,它調用上述那些重載函數並提供traits class 所提供的信息。

TMP模板元編程,執行於C++編譯器,因此可將工作從運行期轉移到編譯期,這導致一個結果是,某些錯誤原本通常在運行期才能偵查到,現在可在編譯器找出來,另一個結果是,使用TMP的C++程序可能在每一個方面都更高效。traits就是TMP形式的

TMP並沒有真正的循環構件,所以循壞效果由遞歸完成,TMP主要是個函數式語言,TMP的遞歸甚至不是正常種類,因爲TMP循環並不涉及遞歸函數調用,而是涉及遞歸模板具現化。一個計算階乘的例子遞歸表現如下:

template<unsigned n>
struct Factorial{
 enum{ value=n* Factorial<n-1>::value};
};
template<>
struct Factorial<0>{
 enum{value=1};
};

STL容器所使用的堆內存是由容器所擁有的分配器對象(allocator objects)管理

當operator new 拋出異常以反映一個未獲滿足的內存需求之前,它會先調用一個客戶指定的錯誤處理函數

namespace std{
 typedef void (*new_handler)();
 new_handler set_new_handler(new_handler p) throw();
}

new_handler是個typedef,定義出一個指針指向函數,該函數沒有參數也不返回任何東西。set_new_handler則是獲得一個new_handler並返回一個new_handler的函數,set_new_handler聲明式尾端的throw()是一份異常明細,表示該函數不拋出任何異常。當operator new無法滿足內存申請時,它會不斷調用new handler函數,直到找到足夠內存,一個良好設計的new-handler函數必須做下事情:1.讓更多內存可被使用,2.安裝另一個new-handler,3.卸除new-handler(也就是將null指針傳給set_new_handler,一旦沒有安裝任何new-handler,operator new會在內存分配不成功時拋出異常)4.拋出bad_alloc的異常,5.不返回,直接調用abort。

在new的使用場合用了nothrow對象,widget* pw2=new (std::nothrow) widget;表達式說明當operator new被調用,用以分配足夠內存給Widget對象,如果分配失敗便返回Null指針,雖然它不會拋出異常,但是類的構造函數卻會。

考慮替換operator new或operator delete一般出於下面三個常見理由:用來檢測運用上的錯誤,爲了強化效果(當內存被分得太小被無法滿足大內存需求,或因爲分得太大無法滿足小需求),爲了收集使用上的統計數據。爲了增加分配和歸還得速度,爲了降低缺省內存管理器帶來的空間額外開銷,爲了彌補缺省分配器中的非最佳齊位,爲了將相關對象成簇集中。

即使客戶要求0bytes,operator new也得返回一個合法指針。operator new成員函數會被派生類繼承!

當定義自己的new和delte時需固守常規,如果你決定寫個operator new[],記住唯一需要做的事情就是分配一塊未加工內存

class Base{
 public:
 static void* operator new(std::size_t size) throw(std::bad_alloc);
 static void operator delete(void* rawMemory.std::size_t size) throw();
 ...
};
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
 if(size!=sizeof(Base))          //如果大小錯誤
    return ::operator new(size); //令標準的operator new來處理
 ...                              //否則在這裏處理
}
void Base::operator delete(void* rawMemory,std::size_t size) throw();
  if(rawMemory==0) return;            //檢查空指針,什麼都不用做直接返回       C++保證,刪除null指針永遠安全
 if(size!=sizeof(Base)){                 //如果大小錯誤,令標準版本來處理
 ::operator delete(rawMemory);
 return ;
} return;              //現在,歸來rawMemory所指內存
};

如果operator new接受的參數除了一定會有的那個size_t之外還有其他,這便是所謂的placement new,衆多版本中,有一個特別有用的是接受一個指針指向對象該被構造之處,長相爲

void* operator new(std::size_t,void* pMemory) throw();
//這個new的用途之一是負責在vector的未使用空間上創建對象

考慮如下

Widget* pw=new (std::cerr) Widget;

如果內存分配成功,而Widget構造函數拋出異常,運行期系統有責任取消operator new分配並恢復原樣,然後運行期系統無法知道真正被調用的那個operator new如何運作,因此它無法取消分配並恢復原樣,運行期系統尋找參數個數和類型都與operator new相同的某個opereator delete,如果找到就調用它,所以寫了placement new也要寫placement delete,如果沒有寫,那麼當placement new調用失敗時就沒有operator delete被調用

缺省情況下C++在global作用域內提供以下形式的operator new

void* operator new(std::size_t) throw(std::bad_alloc);   //normal new
void* operator new(std::size_t,void*) throw()          //placement new
void* operator new(std::size_t,const std::nothrow_t&) throw();  //nothrow new

除非你的意識是要阻止class的客戶使用這些形式,否則請確保它們在你所生成的任何定製型operator new之外還可以使用

class Base{
 ...
 static void* operator new(std::size_t size,std::ostream& logStream)
 throw(std::bad_alloc);
 ...
};
Base* pb=new Base;             //錯誤,正常形式的operator new被掩蓋
Base* pb=new(std::cerr) Base;   //正確調用Base的placement new
class Dervied:public Base{
 public:
 ...
 static void* operator new(std::size_t size) throw (std::bad_alloc); //重新聲明正常形式的New
...
};
Derived* pd=new (std::clog) Derived;  //錯誤,Base的placement new被掩蓋了
Derived* pd=new Derived;           //沒問題,調用Derived的operator new

解決方法爲,在類裏聲明所有正常形式的new和delete,或者繼承標準形式,然後使用using,讓基類函數在派生類中可見

需要多注意TR1中詳細敘述了的14個新組件。

發佈了47 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章