1. 程式人生 > >C++Primer_Chap16_模板和泛型程式設計_List01_定義模板_筆記

C++Primer_Chap16_模板和泛型程式設計_List01_定義模板_筆記

  面向物件變成(OOP)和泛型程式設計都能處理在編寫程式時不知道型別的情況。不同之處在於:

  • OOP能處理型別在程式執行之前都未知的情況
  • 泛型程式設計中,在編譯時就能獲知型別。

函式模板

  我們可以定義一個通用的函式模板(function template),一個函式模板就是一個公式,可生成針對特定型別的函式版本

template <typename T>
int compare( const T &val1, const T &val2)
{
    if( val1 < val2) 
        return -1;
    if( val2 < val1)
        return 1;
    return 0;
}

  模板定義以關鍵字template開始,後跟一個模板引數列表(template parameter list),這是一個逗號分隔的一個或多個模板引數(template parameter)的列表,用<>包圍起來。

  一般來說,可以將型別引數看做型別說明符,就想內建型別或類型別說明符一樣使用。特別是,型別引數可以用來指定返回型別或函式的引數,以及在函式體內用於宣告變數或型別轉換。型別引數前必須使用關鍵字class或typename(在模板引數列表中,class和typename沒有什麼不同):

template<typename T, class U> calc(const T&, const U&);

非型別模板引數

  除了定義型別引數,還可以在模板中定義非型別引數(nontype parameter)。一個非型別引數表示一個值而非一個型別。通過一個特定的型別名而非關鍵字class或typename來指定非型別引數。

  當一個模板被例項化時,非型別引數被一個使用者提供的或編譯器推斷出的值所代替。這些值必須是常量表達式,從而允許編譯器在編譯時時例項化模板。

template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char(&p2)[M])
{
    return strcmp(p1, p2);
}

compare("hi", "mom");
int compare( const char (&p1)[3], const char (&p2)[4]);

  編譯器會使用字面常量的大小代替N和M,從而例項化模板。一個非型別引數可以是一個整型,或者是一個指向物件或函式型別的指標或(左值)引用。繫結到非型別整型引數的實參必須是一個常量表達式。繫結到指標或引用非型別引數實參必須具有靜態的生存期。我們不能用一個普通(非static)區域性變數或動態變數作為指標或引用非型別模板引數的實參。指標引數也可以用nullptr或值為0的常量表達式來例項化。

inline和constexpr的函式模板

  inline和constexpr說明符放在模板引數列表之後,返回型別之前:

template<typename T> inline T min(const T&, const T&);

  編寫泛型程式碼的兩個重要原則:

  • 模板中的函式引數是const的引用(保證了函式可以用於不能拷貝的型別)
  • 函式體中的條件判斷僅使用<比較運算(降低compare對要處理的型別的要求,只需要有<,不必同時支援>)

  實際上,如果真的關心型別無關和移植性,可能需要使用less來定義我們的函式.(彌補原版本針對兩個指標,且兩個指標未指向相同的資料時程式碼行為未定義的問題)

template<typename T> int compare( const T &v1, const T &v2)
{
    if( less<T>()(v1, v2))
        return -1;
    if( less<T>()(v2, v1))
        return 1;
    return 0;
}

  模板程式應該儘量減少對實參型別的要求。

模板編譯

  當編譯器遇到一個模板定義時,它並不生成程式碼。只有例項化出模板的一個特定版本時,編譯器才會生成程式碼。即當我們使用而不是定義模板時,編譯器才生成程式碼,該特性會影響如何組織程式碼以及錯誤何時被檢測到。

  為了生成一個例項化版本,編譯器需要掌握函式模板和類模板成員函式的定義。因此,與非模板程式碼不同,模板的標頭檔案通常保護宣告和定義。

  類模板

  類模板(class template)是用來生成類的狼途的。和函式模板不同之處是,編譯器不能為類模板推斷模板引數型別。為了使用類模板,我們必須在模板名後面的尖括號中提供額外資訊——用來代替模板引數的模板實參列表

template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;

    Blob();
    Blob( std::initializer_list<T> i1);

    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
	
    void push_back( const T &t) { data->push_back(t); }
    void push_back( const T &&t) { data->push_back(std::move(t)); }
    void pop_back();
	
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string &msg) const;
};

  例項化類模板時必須提供額外的資訊。我們現在知道浙西額額外資訊是顯式模板實參(explicit template argument)列表,他們被繫結到模板引數:

Blob<int> ia;
Blob<int> ia2 = {0, 1, 2, 3, 4};

 例項化會讓編譯器例項化出一個與下面定義等價的類:

template <> class Blob<int> {
public:
    typedef typename std::vector<int>::size_type size_type;

    Blob();
    Blob( std::initializer_list<int> i1);

    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
	
    void push_back( const int &t) { data->push_back(t); }
    void push_back( const int &&t) { data->push_back(std::move(t)); }
    void pop_back();
	
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg) const;
};

  一個類模板的每個例項都會形成一個獨立的類。型別Blob<string>和任何其他Blob型別都沒有關聯,也不會對任何其他Blob型別的成員有特殊訪問許可權。

類模板的成員函式

  類模板的成員函式具有和模板相同的模板引數。因此,定義在類模板之外的成員函式就必須以關鍵字template開始,後接類模板引數列表。

ret-type StrBlob::member-name(parm-list)
{}

template <typename T>
ret-type Blob<T>::member-name(parm-list)
{}

template <typename T>
Blob<T>::Blob() : data( std::make_shared<std::vector<T>>()) 
{}

template <typename T>
Blob<T>::Blob( std:: initializer_list<T> i1) : 
                data( std::make_shared<std::vector<T>>(i1))
 {}

template <typename T>
void Blob<T>::pop_back()
{
	check( 0, "back on empty Blob");
	return data->pop_back();
}

template <typename T>
T& Blob<T>::back()
{
	check( 0, "back on empty Blob");
	return data->back();
}

template <typename T>
T& Blob<T>::operator[](size_type i)
{
	check(i, "subscript out of range");
	return (*data)[i];
}

template <typename T> 
void Blob<T>::check( size_type i, const std::string &msg) const
{
	if( i >= data->size() )
		throw std::out_of_range(msg);
}

  如果一個成員函式沒有被使用,則它不會被例項化。成員函式只有在被用到時才進行例項化,這一特性使得即使某種型別不能完全符合模板操作的要求,我們仍然能用該型別例項化類。

在類程式碼內簡化模板類名的使用

  當我們使用一個類模板型別時必須提供模板實參,但例外是在類模板自己的作用域中,我們可以直接使用模板名而不提供實參:

template <typename T> class BlobPtr
{
public:
    BlobPtr():curr(0) {}
    BlobPtr( Blob<T> &a, size_t sz = 0):wptr(a.data),curr(sz) {}
	
    T& operator*() const
    {
        auto p = check( curr, "dereference past end");
        return (*p)[curr];
    }
    BlobPtr& operator++();
    BlobPtr& operator--();
private:
    std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;	
    std::weak_ptr< std::vector<T>> wptr;
    std::size_t curr;
}; 

    前置遞增/遞減返回BlobPtr&而不是BlobPtr<T>&,當我們處於一個類模板的作用域中時,編譯器處理模板自身引用時就好像我們已經提供了與模板引數匹配的實參一樣。

  在類模板外定義其成員時,必須記住並不在類的作用域中,直到遇到類名才表示進入類的作用域.

template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    BlobPtr ret = *this;
    ++*this;
    return ret;
}

類模板和友元

  當一個類包含一個有友元宣告時,類和友元各自是否是模板是相互無關的。如果一個類模板包含一個非模板友元,則友元被授權可以訪問所有模板示例。如果友元自身是模板,類可以授權給所有友元模板例項,也可以值授權給特定例項。

一對一友好關係

  類模板與另一個(類或函式)模板間友好關係的最常見的形式是建立對應例項及其友元間的友好關係。為了引用(類或函式)模板的一個特定例項,我們必須首先宣告模板自身(一個模板的宣告包括模板引數列表):

template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob{
    friend class BlobPtr<T>;
    friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
}; 

  前面三行的宣告是函式的引數宣告和Blob中的友元宣告所需要的。

  友元宣告用Blob的模板形參作為它們自己的模板實參。因此,友好關係被限定在用相同型別例項化的Blob和BlobPtr相等運算子之間。

Blob<char> ca;
Blob<char>
operator==<char>

Blob<int> ia;
Blob<int>
operator==<int>

通用和特定的模板友好關係

  一個類也可以將另一個模板的每個例項都宣告為自己的友元,或者限定特定的示例為友元:

template <typename T> class Pal;

class C{

    friend class Pal<C>;    //用C例項化的Pal是C的一個友元

    //Pal2的所有示例都是C的友元,這種情況無需前置宣告
    template <typename T> friend class Pal2;
};

template <typename T> class C2 {

    //C2的每個例項將相同例項化的Pal宣告為友元
    friend class Pal<T>;    //Pal的模板宣告必須在作用域之內

    //Pal2的所有示例都是C2的每個例項的友元,不需要前置宣告
    template <typename X> friend class Pal2;

    //Pal3是一個非模板類,它是C2所有示例的友元
    friend class Pal3;
};

  為了讓所有例項稱為友元,友元宣告中必須使用與類模板本身不同的模板引數。

令模板自己的型別引數稱為友元

  可以將模板型別引數宣告成友元:

template <typename Type> class Bar{
    friend Type;
};

模板類型別名

  我們可以定義一個typedef來引用例項化的類,但不能定義一個typedef引用一個模板。不過,我們可以為類模板定義一個類型別名:

typedef Blob<string> StrBlob;

template<typename T> using twin = pair<T, T>;
twin<string> authors;    //authors是一個pair<string, string>
twin<int> win_loss;       //win_loss是一個pair<int, int>
twin<double> area;

類模板的static成員

  類模板的static資料成員必須有且僅有一個定義。類模板的每個例項都有一個獨有的static物件。與定義模板的成員函式類似,我們將static資料成員也定義為模板:

template <typename T> class Foo {
public:
    static std::size_t count() {return ctr;}
private:
    static std::size_t ctr;
};

template <typename T>
size_t Foo<T>::ctr = 0;


Foo<int> fi;
auto ci = Foo<int>::count();
ct = fi.count();
ct = Foo::count();    //錯誤

模板引數

  類似函式引數的名字,一個模板引數的名字沒有什麼內在含義,其可用範圍是在其什麼之後,至模板宣告或定義結束之前(會隱藏外層作用域中宣告的相同名字)。引數名不能重用,但定義中的名字不必與宣告中的模板引數名字相同。

使用類的型別成員

  假定T是一個模板型別引數,當編譯器遇到類似T::mem這樣的程式碼時,它不會知道men是一個型別成員還是一個static資料成員,直到例項化。但為了處理模板,編譯器必須指定名字是否表示一個型別。

T::size_type * p;
//是定義一個p的指標變數?
//還是將一個名為size_type的static資料成員與p相乘

  預設情況下,C++語言假定通過作用域訪問符(::)訪問的名字都不是型別。因此,如果希望使用一個模板型別引數的型別名字,必須顯式告訴編譯器該名字是一個型別(通過使用關鍵字typename來實現):

template <typename T>
typename T::value_type top(const T& c)
{    
    if( !c.empty())
        return c.back();
    else
        return typename T::value_type();
}

  當我們希望通知編譯器一個名字表示型別時,必須使用關鍵字typename,而不能使用class

預設模板實參

  我們可以提供預設模板實參(default template argument)。在C++11標準後,可以為函式和類模板提供預設實參。

template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
    if( f(v1, v2))
        return -1;
    if( f(v2, v1))
        return 1;
    return 0;
}

模板預設實參和類模板

無論何時使用一個類模板,都必須在模板名之後接上尖括號。尖括號指出類必須從一個模板例項化而來。特別,如果一個類模板為其所有模板引數都提供了預設實參,且希望使用這些預設實參,就必須在模板名之後跟一個空尖括號對:

template<class T = int> class Numbers{
    //預設T為int
public:
    Numbers(T v = 0) : val(v) {}
private:
    T val;
};

Numbers<long double> lots_of_precision;
Numbers<> average_precision;

成員模板

  一個類(無論是普通類還是類模板)可以包含本身是模板的成員函式。這種成員稱為成員模板(member template)。成員模板不是虛擬函式。

  過載的函式呼叫運算子希望刪除器適用於任何型別,所以將呼叫運算子定義為一個模板:

class DebugDelete {
public:
    DebugDelete(std::ostream &os = std::cerr ) : os(s) {}
    template <typename T> void operator()(T *p) const
    {    
        os << "deleting unique_ptr" << std::endl;
        delete p;        
    }
private:
    std::ostream &os;
};


double *p = new double;
DebugDelete d;
//呼叫DebugDelete::operator()(double *),釋放p
d(p);

int *ip = new int;
//在一個臨時DebugDelete物件上呼叫operator()(int *)
DebugDelete()(ip);

  由於呼叫一個DebugDelete物件會delete其給定指標,我們可以將其用作unique_ptr的刪除器。為了過載unique_ptr的刪除器,我們在尖括號內給出了刪除器型別,並提供一個這種型別的物件給unique_ptr的建構函式:

unique_ptr<int, DebugDelete> p(new int, DebugDelete());
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

類模板的成員模板

  對於類模板,也可以為其定義成員模板:

template <typename T> class Blob {
    template <typename IT> Blob(It b, It e);

};

  與類模板的普通成員函式不同,成員模板是函式模板。當在類模板外定義一個成員模板時,必須同時為類模板和成員模板提供模板引數列表。類模板的引數列表在前,後跟成員自己的模板引數列表:

template <typename T>
template <typename IT>
    Blob<T>::Blob(IT b, IT e) : 
        data(std::make_shared<std::vector<T>>(b, e) ) 
    { }

例項化和成員模板

  為了例項化一個類模板的成員模板,我們必須同時提供類和函式模板的實參。與普通函式模板相同,編譯器通常根據傳遞給成員模板的函式實參來推斷它的模板型別:

int ia[] = {0,1,2};
vector<long> vi = {0, 1, 2, 3};
list<const char*> w = {"now", "is", "the", "time"};

//例項化Blob<int>類及接受兩個int*引數的建構函式
Blob<int> a1(begin(ia), end(ia));

//例項化Blob<int>類及接受兩個vector<long>::iterator的建構函式
Blob<int> a2(vi.begin(), vi.end());

//例項化Blob<string>類及接受兩個list<const char*>::iterator的建構函式
Blob<string> a2(w.begin(), w.end());

控制例項化

當模板被使用時才進行例項化這特性意味著相同的例項可能會出現在多個物件檔案中(.o)。在大系統中,在多個檔案中例項化化相同模板的額外開銷可能非常嚴重。可以通過顯示例項化(explicit instantiation)來避免這種開銷:

extern template declaration;    //例項化宣告
template declaration;            //例項化定義

  declaration是一個類或函式宣告,其中所有模板引數已被替換成模板實參,例如:

extern template class Blob<string>;        //宣告
template int compare(const int&, const int&);    //定義

  由於編譯器你在使用一個模板時自動對其例項化。因此extern宣告必須出現在任何使用此例項化版本的程式碼之前。

// Application.cc
//這些模板型別必須在程式其他位置進行例項化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;    //例項化會出現在其他位置

//Blob<int>及其接受initializer_list的建構函式在本檔案中例項化
Blob<int> a1 = {0, 1, 2, 3, 4, 5, 6};
Blob<int> a2(a1);    //拷貝建構函式在本檔案中例項化
int i = compare(a1[0], a2[0]);    //例項化出現在其他位置

   檔案Application.o將包含Blob<int>的例項及其接受initializer_list引數的建構函式和拷貝建構函式的例項。而compare<int>函式和Blob<string>類將不再本檔案中進行例項化。這些模板的定義必須出現在程式的其他檔案中:

//templateBuild.cc
//例項化檔案必須為每個在其他檔案中宣告為extern的型別和函式提供一個(非extern)的定義
template int compare(const int&, const int&);
template class Blob<string>;

   當編譯器遇到一個例項化定義(與宣告相對)時,它為其生存程式碼。因此templateBuild.o將包含compare的int例項化版本的定義和Blob<string>類的定義。

  對於每個例項化宣告,在程式中某個位置必須有其顯式的例項化定義。例項化定義會例項化所有成員:一個類模板的例項化定義會例項化該模板的所有成員,包括內聯的成員函式。當編譯器遇到一個例項化定義時,它不瞭解程式使用那些成員函式。因此與處理類模板的普通例項化不同,編譯器會例項化該類的所有成員。因此,我們用來顯示例項化一個類模板的型別,必須能用於模板的所有的成員。