1. 程式人生 > >[轉]C++ template —— 模板基礎(一)

[轉]C++ template —— 模板基礎(一)

分配 博客 講解 變參 ice allocator 不可 nts 枚舉值

《C++ Template》對Template各個方面進行了較為深度詳細的解析,故而本系列博客按書本的各章順序編排,並只作為簡單的讀書筆記,詳細講解請購買原版書籍(絕對物超所值)。
------------------------------------------------------------------------------------------------------------
第一章 前言
1.4 編程風格
(1)對“常整數”趨向使用“int const”,而不是使用“const int”。“恒定不變部分”指的是const限定符前面的部分。
------------------------------------------------------------------------------------------------------------
第1部分 基礎
第2章 函數模板
2.1 初探函數模板
2.1.1 定義模板

template <typename T>
inline T const& max(T const& a, T const& b)
{
    return a < b ? : b a;
}

註:你可以使用任何類型(基本類型、類等)來實例化該類型參數,只要所用類型提供模板使用的操作就可以。
2.1.2 使用模板

max(32, 43);

註:通常而言,並不是把模板編譯成一個可以處理任何類型的單一實體;而是對於實例化模板參數的每種類型,都從模板產生一個不同的實體。這種用具體類型代替模板參數的過程叫做實例化,它產生了一個模板的實例。
於是,我們可以得出一個結論:模板被編譯了兩次,分別發生在:
(1)實例化之前,先檢查模板代碼本身,查看語法是否正確;在這裏會發現錯誤的語法,如遺漏分號等。
(2)在實例化期間,檢查模板代碼,查看是否所有的調用都有效。在這裏會發現無效的調用,如該實例化類型不支持某些函數調用(該類型沒有提供模板所需要使用到的操作)等。

2.2 實參的演繹(deduction)
註:模板實參不允許進行自動類型轉換;每個T都必須正確地匹配。如:

max(4, 4.3); // Error:第1個參數類型是int,第2個參數類型是double

2.3 模板參數
函數模板有兩種類型的參數(牢記):
(1)模板參數:位於函數模板名稱的前面,在一對尖括號內部進行聲明:

template <typename T>   // T是模板參數

(2)調用參數:位於函數模板名稱之後,在一對圓括號內部進行聲明:

...max(T const& a, T const& b);        // a和b都是調用參數

註:由於函數模板歷史發展過程中的一個失誤,導致目前(2016/1/11)我們不能在函數模板內部指定缺省的模板實參(形如“template <typename T = xxx>”,不能指定xxx;)。但依然可以指定函數模板的調用實參(形如“...max(T const& a, T const& b = yyy)”,可以指定yyy)(以後應該會支持函數模板指定缺省模板實參)。
註:切記“模板參數”和“模板實參”的區別;函數的“模板參數”和“調用參數”、“模板實參”和“調用實參”的區別;“類型參數”和“非類型參數”的區別;
函數模板和類模板區別:函數模板可以進行模板實參演繹(不能演繹返回類型)、重載、指定缺省調用實參、全局特化;不能指定缺省模板實參,不能局部特化;類模板可以指定缺省模板實參、指定全局特化和局部特化(用來完成類似函數模板重載功能);不能重載類模板,不能進行實參演繹。
(3)顯式實例化:當模板參數和調用參數沒有發生關聯,或者不能由調用參數來決定模板參數的時候,在調用時就必須顯式指定模板實參。切記,模板實參演繹並不適合返回類型。如下:

template <typename T1, typename T2, typename RT>
inline RT max(T1 const& a, T2 const& b);

那麽必須進行顯式實例化:

max<int, double, double>(4, 4.3);  // OK,但很麻煩。這裏T1和T2是不同的類型,所以可以指定兩個不同類型的實參4和4.3

註:通常而言,你必須指定“最後一個不能被隱式演繹的模板實參之前的”所有實參類型。上面的例子中,改變模板參數的聲明順序,那麽調用者就只需要指定返回類型:

template <typename RT, typename T1, typename T2>
inline RT max(T1 const& a, T2 const& b);
...
max<double>(4, 4.3); // ok,返回類型是double

2.4 重載函數模板
註:對於非模板函數和同名的函數模板,如果其他條件都是相同的話,那麽在調用的時候,重載解析過程通常會優先調用非模板函數,而不會從該模板產生出一個實例。然而,如果模板可以產生一個具有更好匹配的函數,那麽將選擇模板。
註:可以顯式地指定一個空的模板實參列表,這個語法好像是告訴編譯器:只有模板才能匹配這個調用(即便非模板函數更符合匹配條件也不會被調用到),而且所有的模板參數都應該根據調用實參演繹出來。

註:因為模板是不允許自動類型轉化的;但普通函數可以進行自動類型轉換,所以當一個匹配既沒有非模板函數,也沒有函數模板可以匹配到的時候,會嘗試通過自動類型轉換調用到非模板函數(前提是可以轉換為非模板函數的參數類型)。
註:在所有重載的實現裏面,我們都是通過引用來傳遞每個實參的。一般而言,在重載函數模板的時候,最好只是改變那些需要改變的內容;就是說,你應該把你的改變限制在下面兩種情況:改變參數的數目或者顯式地指定模板參數。否則可能會出現非預期的結果。(參考書本2.4節例子)
註:定義一個重載函數A,而在A1(函數A的重載)中調用A,但是,如果直到A1的定義處還沒有見到A的定義(也即函數A的定義在函數A1的後面,但函數A1中調用了函數A),那麽並不會調用到這個重載函數A,而會尋找在函數A1之前已經定義了的符合條件的其他函數Ax(即便A是符合條件的非模板函數,而Ax是模板函數,也會由於A的聲明太遲,而選擇調用Ax)。


------------------------------------------------------------------------------------------------------------
第3章 類模板
3.1 類模板Stack的實現
見書源碼;
3.1.1 類模板的聲明

template <typename T>    //可以使用class代替typename
class Stack
{
    ...
};

註:這個類的類型是Stack<T>,其中T是模板參數。因此,當在聲明中需要使用該類的類型時,你必須使用Stack<T>。然而,當使用類名而不是類的類型時,就應該只用Stack;譬如,當你指定類的名稱、類的構造函數、析構函數時,就應該使用Stack。
3.1.2 成員函數的實現
為了定義類模板的成員函數,你必須指定該成員函數是一個函數模板,而且你還需要使用這個類模板的完整類型限定符。如下:

template <typename T>
void Stack<T>::push(T const& elem)
{
    elems.push_back(elem);
}

顯然,對於類模板的任何成員函數,你都可以把它實現為內聯函數,將它定義於類聲明裏面,如:

技術分享圖片
template <typename T>
class Stack
{
    ...
    void push(T const& elem)
    {
        elems.push_back(elem);
    }
    ...
};
技術分享圖片

3.2 類模板Stack的使用
為了使用類模板對象,必須顯式地指定模板實參。
註:
1. 只有那些被調用的成員函數,才會產生這些函數的實例化代碼。對於類模板,成員函數只有在被使用的時候才會被實例化。顯然,這樣可以節省空間和時間;
2. 另一個好處是,對於那些“未能提供所有成員函數中所有操作的”類型,你也可以使用該類型來實例化類模板,只要對那些“未能提供某些操作的”成員函數,模板內部不使用就可以。
3. 如果類模板中含有靜態成員,那麽用來實例化的每種類型,都會實例化這些靜態成員。
切記,要作為模板參數類型,唯一的要求就是:該類型必須提供被調用的所有操作。
3.3 類模板的特化
為了特化一個類模板,你必須在起始處聲明一個template<>,接下來聲明用來特化類模板的類型。這個類型被用作模板實參,且必須在類名的後面直接指定:

template<>
class Stack<std::string>
{
    ...
};

進行類模板的特化時,每個成員函數都必須重新定義為普通函數,原來模板函數中的每個T也相應地被進行特化的類型取代。如:

void Stack<std::string>::push(std::string const& elem)
{
    elems.push_back(elem);
}

3.4 局部特化
例子如下:

技術分享圖片
// 兩個模板參數具有相同的類型
template <typename T>
class Myclass<T, T>   // 
{
};

// 第2個模板參數的類型是int
template <typename T>
class Myclass<T, int>
{
};

// 兩個模板參數都是指針類型
template <typename T1, typename T2>
class Myclass<T1*, T2*>     // 也可以使引用類型T&,常引用等
{
};
技術分享圖片

3.5 缺省模板實參
對於類模板,你還可以為模板參數定義缺省值;這些值就被稱為缺省模板實參;而且它們還可以引用之前的模板參數。(STL容器使用缺省默認實參指定內存分配其alloc)如:

template <typename T, typename CONT = std::vector<T> >
class Stack
{
};

------------------------------------------------------------------------------------------------------------
第4章 非類型模板參數
4.1 非類型的類模板參數
如下定義:

技術分享圖片
template <typename T, int MAXSIZE>
class Stack
{
};
// 使用
Stack<int, 20> int20Stack;     // 可以存儲20個int元素的棧
Stack<int, 40> int40Stack;     // 可以存儲40個int元素的棧
技術分享圖片

註:每個模板實例都具有自己的類型,因此int20Stack和int40Stack屬於不同的類型,而且這兩種類型之間也不存在顯式或者隱式的類型轉換;所以它們之間不能互相替換,更不能互相賦值。
然而,如果從優化設計的觀點來看,這個例子並不適合使用缺省值。缺省值應該是直觀上正確的值。但對於棧的類型和大小而言,int類型和最大容量100從直觀上看起來都不是正確的。因此,在這裏最好還是讓程序員顯式地指定這兩個值。因此我們可以在設計文檔中用一條聲明來說明這兩個屬性(即類型和最大容量)。
4.2 非類型的函數模板參數
如下定義:

template <typename T, int VAL>
T addValue(T const& x)
{
    return x + VAL;
}

借助標準模板庫(STL)使用上面例子:

std::transform(source.begin(), source.end(), dest.begin(), addValue<int, 5>);

註:
1. 上面的調用中,最後一個實參實例化了函數模板addValue(),它讓int元素增加5.
2. 這個例子有一個問題:addValue<int, 5>是一個函數模板實例,而函數模板實例通常被看成是用來命名一組重載函數的集合(即使該組只有一個函數)。然而,根據現今的標準,重載函數的集合並不能被用於模板參數的演繹(註意,標準模板庫中的函數是使用模板定義的,故而在transform()函數中,參數是作為函數模板調用實參傳遞的,也即參與了模板參數演繹)。於是,你必須將這個函數模板的實參強制類型轉換為具體的類型:

std::transform(source.begin(), source.end(), dest.begin(), (int(*)(int const&))addValue<int, 5>);

4.3 非類型模板參數的限制
我們還應該知道:非類型模板參數是有限制的。通常而言,它們可以是常整數(包括枚舉值)或者指向外部鏈接對象的指針。
註:浮點數和類對象(class-type)是不允許作為非類型模板參數的。之所以不能使用浮點數(包括簡單的常量浮點表達式)作為模板實參是有歷史原因的。然而以後可能會支持這個特性。另外,由於字符串文字是內部鏈接對象(因為兩個具有相同名稱但出於不同模塊的字符串,是兩個完全不同的對象),所以你不能使用它們來作為模板實參,如下:

技術分享圖片
template <char const* name>
class MyClass
{
};
MyClass<"hello"> x;   // ERROR:不允許使用字符文字"hello"
//另外,你也不能使用全局指針作為模板參數:
template <char const* name>
class MyClass
{
    ...
};
char const* s = "hello";
MyClass<s> x;   // s是一個指向內部鏈接對象的指針
//然而,你可以這樣使用:
template <char const* name>
class MyClass
{
    ...
};
extern char const s[] = "hello";
MyClass<s> x;   // OK
//全局字符數組s由“hello”初始化,是一個外部鏈接對象。
技術分享圖片


------------------------------------------------------------------------------------------------------------
第5章 技巧性基礎知識
5.1 關鍵字typename
在C++標準化過程中,引入關鍵字typename是為了說明:模板內部的標識符可以是一個類型。如下:

技術分享圖片
template <typename T>
class MyClass
{
    // 這裏的typename被用來說明:T::SubType是定義於類T內部的一種類型
    typename T::SubType* ptr;
    ...
};
技術分享圖片

註:本節同時提到了一個奇特的構造“.template”:只有當該前面存在依賴於模板參數的對象時,我們才需要在模板內部使用“.template”標記(和類似的諸如->template的標記),而且這些標記也只能在模板中才能使用。如下例子:

技術分享圖片
void printBitset(std::bitset<N> const& bs)
{
    // 如果沒有使用這個template,編譯器將不知道下列事實:bs.template後面的小於號(<)並不是數學
    // 中的小於號,而是模板實參列表的起始符號。只有在編譯器判斷小於號之前,存在依賴於模板參數的構造
    // 才會出現這種問題。在這個例子中,傳入參數bs就是依賴於模板參數N的構造
    std::cout << bs.template to_string<char, char_traits<char>, allocator<char> >();
}
技術分享圖片

5.2 使用this->
考慮例子:

技術分享圖片
template <typename T>
class Base
{
    public:
        void exit();
};

template <typename T>
class Derived : Base<T>    // 模板基類
{
    public:
        void foo()
        {
             exit();    // 調用外部的exit()或者出現錯誤,而不會調用模板基類的exit()
        }
};
技術分享圖片

註意:對於那些在基類中聲明,並且依賴於模板參數的符號(函數或者變量等),你應該在它們前面使用this->或者Base<T>::限定符。如果希望完全避免不確定性,你可以(使用諸如this->和Base<T>::等)限定(模板中)所有的成員訪問。(這兩種限定符的詳細信息會在本系列文章後面講解)

參見博文Effective C++ —— 模板與泛型編程(七) 條款43
5.3 成員模板
對於類模板而言,其實例化只有在類型完全相同才能相互賦值。我們通過定義一個身為模板的賦值運算符(成員模板),來達到兩個不同類型(但類型可以轉換)的實例進行相互賦值的目的,如下聲明:

技術分享圖片
template <typename T>
class Stack
{
    ...
    template <typename T2>
        Stack<T>& operator= (Stack<T2> const&);
};
技術分享圖片

參見博文Effective C++ —— 模板與泛型編程(七) 條款45

5.4 模板的模板參數
還是以Stack為例:

技術分享圖片
template <typename T, 
                template <typename ELEM,
                                    typename ALLOC = std::allocator<ELEM> > 
                                        class CONT = std::deque >
class Stack
{
    ...
};
技術分享圖片

註:
1. 上面作為模板參數裏面的class 不能用typename代替;
2. 還有一個要知道:函數模板並不支持模板的模板參數。
3. 之所以需要定義“ALLOC”,是因為模板的模板實參“std::deque”具有一個缺省模板參數,為了精確匹配模板的模板參數;
5.5 零初始化
對於int、double或者指針等基本類型,並不存在“用一個有用的缺省值來對它們進行初始化”的缺省構造函數;相反,任何未被初始化的局部變量都具有一個不確定值。如果我們希望我們的模板類型的變量都已經用缺省值初始化完畢,那麽針對內建類型,我們需要做一些處理,如下:

技術分享圖片
// 函數模板
template <typename T>
void foo()
{
    T x = T();   // 如果T是內建類型,x是0或者false
};
// 類模板:初始化列表來初始化模板成員
template <typename T>
class MyClass
{
    private:
        T x;
    public:
        MyClass() : x() {}  // 確認x已被初始化,內建類型對象也是如此
};
技術分享圖片

5.6 使用字符串作為函數模板的實參
有時,把字符串傳遞給函數模板的引用參數會導致出人意料的運行結果:

技術分享圖片
#include <string>
// 註意,method1:引用參數
template <typename T>
inline T const& max(T const& a, T const& b)
{
    return a < b ? b : a;
}
// method2:非引用參數
template <typename T>
inline T max2(T a, T  b)
{
    return a < b ? b : a;
}

int main()
{
    std::string s;
    // 引用參數
    ::max("apple", "peach");    // OK, 相同類型的實參
    ::max("apple", "tomato");    // ERROR, 不同類型的實參
    ::max("apple", s);      // ERROR, 不同類型的實參
    // 非引用參數
    ::max2("apple", "peach");    // OK, 相同類型的實參
    ::max2("apple", "tomato");    // OK, 退化(decay)為相同類型的實參
    ::max2("apple", s);      // ERROR, 不同類型的實參
}
技術分享圖片

上面method1的問題在於:由於長度的區別,這些字符串屬於不同的數值類型。也就是說,“apple”和“peach”具有相同的類型char const[6];然而“tomato”的類型則是char const[7]。
method2調用正確的原因是:對於非引用類型的參數,在實參演繹的過程中,會出現數組到指針的類型轉換(這種轉型通常也被稱為decay)。
小結:
如果你遇到一個關於字符數組和字符指針之間不匹配的問題,你會意外地發現和這個問題會有一定的相似之處。這個問題並沒有通用的解決方法,根據不同情況,你可以:
1. 使用非引用參數,取代引用參數(然而,這可能會導致無用的拷貝);
2. 進行重載,編寫接收引用參數和非引用參數的兩個重載函數(然而,這可能會導致二義性);
3. 對具體類型進行重載(譬如對std::string進行重載);
4. 重載數組類型,譬如:

template <typename T, int N, int M>
T const* max (T const (&a)[N], T const (&b)[M])
{
    return a < b ? b : a;
}

5. 強制要求應用程序程序員使用顯式類型轉換。
對於我們的例子,最好的方法是為字符串重載max().無論如何,為字符串提供重載都是必要的,否則比較的將是兩個字符串的地址。

------------------------------------------------------------------------------------------------------------
第6章 模板實戰
6.1 包含模型
6.1.1 連接器錯誤
大多數C和C++程序員會這樣組織他們的非模板代碼:

1. 類(class)和其他類型(other type)都被放在一個頭文件中。通常而言,頭文件是一個擴展名為.hpp(或者.H, .h, .hh, hxx)的文件;
2. 對於全局變量和(非內聯)函數,只有聲明放在頭文件中,定義則位於dot-C文件。通常而言,dot-C文件是指擴展名為.cpp(或者.C, .c, .cc, .cxx)的文件。
這樣一切都可以正常運作了。所需的類型定義在整個程序中都是可見的;並且對於變量和函數而言,鏈接器也不會給出重復定義的錯誤。
但這種情況在模板中會出現一些問題,如下:

技術分享圖片
// -----------------------------------------------------------
//basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP

// 模板聲明
template <typename T>
void print_typeof(T const&)

#endif   // MYFIRST_HPP

// -----------------------------------------------------------
//basics/myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"

// 模板的實現/定義
template <typename T>
void print_typeof(T const& x)
{
    std::cout << typeid(x).name() << std::endl;
}

// -----------------------------------------------------------
//basics/myfirstmain.cpp
#include "myfirst.cpp"

// 使用模板
int main()
{
    double ice = 3.0;
    print_typeof(ice);        // 調用參數類型為double的函數模板
}
技術分享圖片

大多數C++編譯器都會順利地接受這個程序;但是鏈接器可能會報錯,提示找不到函數print_typeof()的定義。
事實上,這個錯誤的原因在於:函數模板print_typeof()的定義還沒有被實例化。為了使模板真正得到實例化,編譯器必須知道:應該實例化哪個定義以及要基於哪個模板實參來進行實例化。遺憾的是,在前面的例子裏,這兩部分信息位於分開編譯的不同文件裏面。因此,當我們的編譯器看到print_typeof()調用,但還沒有看到(基於double實例化的)函數定義的時候(在這個時候,或者說在這個條件下),它只能假設在別處提供了這個定義(但它不知道是哪裏提供了),並產生一個指向該定義的引用(這個引用是用來指向該定義的,只不過它目前無法確定,或者說還沒有給這個引用賦值,只能讓鏈接器利用該引用來解決這個問題)。另一方面,當編譯器處理文件myfirst.cpp的時候,它並沒有指出:編譯器必須基於(哪個)特定實參對所包含的模板定義進行實例化(個人理解:前面編譯器把一個引用提供給了鏈接器,希望鏈接器能解決“找不到函數定義”的問題。如果對於普通函數,那麽當編譯器處理文件myfirst.cpp的時候,是可以確定函數定義的,雖然這個函數定義產生於另一個翻譯單元,但可被鏈接器找到;但這裏,我們在myfirst.cpp中定義的是一個函數模板定義,並且也沒有指出“編譯器必須基於(哪個)特定實參對所包含的模板定義進行實例化”(沒有顯示實例化地指出應該根據哪個實參進行實例化),這樣的話,函數模板的定義依然是不確定的,鏈接器在此時便報了找不到函數定義的錯誤)。

要解決上面的問題,可以從兩個點入手:

(1)解決找不到函數模板定義問題(包含模型);

(2)解決沒有指出“編譯器必須基於(哪個)特定實參對所包含的模板定義進行實例化”問題(顯示實例化)。

6.1.2 頭文件中的模板

對於前面的問題,我們通常是采取對待宏或內聯函數的解決方法:我們把模板的定義也包含在聲明模板的頭文件裏面,即讓定義和聲明都位於同一個頭文件中。我們稱模板的這種組織方式為包含模型。針對包含模型的組織方式,我們可以得出:包含模型明顯增加了包含頭文件myfirst.hpp的開銷。
從包含模型得出的另一個結論是:非內聯函數模板與“內聯函數和宏”有一個很重要的區別,那就是非內聯函數模板在調用的位置並不會被擴展,而是當它們基於某種類型進行實例化之後,才產生一份新的(基於該類型的)函數拷貝(所以對於非內聯函數模板而言,實例化之後才能確定為一個針對特定類型的函數)。
最後,我們需要指出的是:在我們的例子中應用到普通函數模板的所有特性,對類模板的成員函數和靜態數據成員、成員函數模板也都是適用的。
6.2 顯式實例化
包含模型能夠確保所有需要的模板都已經實例化。這是因為:當需要進行實例化的時候,C++編譯系統會自動產生所對應的實例化體。另外,C++標準還提供了一種手工實例化模板的機制:顯式實例化指示符。

6.2.1 顯式實例化的例子
為了說明手工實例化,讓我們回顧前面那個導致鏈接器錯誤的例子。在此,為了避免這個鏈接期錯誤,我們可以通過給程序添加下面的文件:

//basics/myfirstinst.cpp
#include "myfirst.cpp"

// 基於類型double顯式實例化print_typeof()
template void print_typeof<double>(double const&);

顯式實例化指示符由關鍵字template和緊接其後的我們所需要實例化的實體(可以是類、函數、成員函數等)的聲明組成,而且,該聲明是一個已經用實參完全(註意,是完全)替換參數之後的聲明。該指示符也適用於成員函數和靜態數據成員,如:

// 基於int顯式實例化MyClass<>的構造函數
template MyClass<int>::MyClass();

// 基於int顯式實例化函數模板max()
template int const& max(int const&, int const&);

你還可以顯式實例化類模板,這樣就可以同時實例化它的所有類成員。但有一點需要註意:對於那些在前面已經實例化過的成員,就不能再次對它們進行實例化(針對每個不同實體,不能存在多個顯式實例化體,同時顯式實例化體和模板特化也只能二者選其一)。

技術分享圖片
// 基於int顯式實例化類Stack<>
template class Stack<int>  // 實例化它的所有類成員
// 錯誤,對於int,不能再次對它進行顯式實例化
template Stack<int>:::Stack();

// 基於string顯式實例化Stack<>的某些成員函數
template Stack<std::string>::Stack();
template void Stack<std::string>::push(std::string const&);
template std::string Stack<std::string>::top() const;
技術分享圖片

人工實例化有一個顯著的缺點:我們必須仔細跟蹤每個需要實例化的實體。對於大項目而言,這種跟蹤會帶來巨大負擔,因此,我們並不建議使用這種方法。其優點在於,顯式實例化可以精確控制模板實例的準確位置。
6.2.2 整合包含模型和顯式實例化
將模板的定義和模板的聲明放在兩個不同的文件中。通常的做法是使用頭文件來表示這兩個文件。如下:

技術分享圖片
// stack.hpp
#ifndef STACK_HPP
#define STACK_HPP
 #include <vector>
 template <typename T>
 class Stack
 {
     private:
         std::vector<T> elems;
     public:
         Stack();
         void push(T const&);
         void pop();
         T top() const;
 };

 #endif

 // stackdef.hpp
 #ifndef STACKDEF_HPP
 #define STACKDEF_HPP

 #include "stack.hpp"

 template <typename T>
 void Stack<T>::push(T const& elem)
 {
     elems.push_back(elem);
 }
 ...

 #endif

 // stacktest1.cpp
 // 註意,這裏和前面鏈接器報錯的例子不同,這裏是包含進了stackdef.hpp,
 // 這個文件裏面含有函數的定義,所以不會產生鏈接器找不到的錯誤(其實在編譯器中就已經能找到函數模板的定義了)
 #include "stackdef.hpp" // 書中是“stack.hpp”,應該有誤
 #include <iostream>
 #include <string>

int main()
{
    Stack<int> intStack;
    intStack.push(42);
}

// stack_inst.cpp
#include "stack.hpp" // 書中是“stackdef.hpp”,應該有誤
#include <string>

template Stack<int>;

template Stack<std::string>::Stack();
template void Stack<std::string>::push(std::string const&);
template std::string Stack<std::string>::top() const;
技術分享圖片

6.3 分離模型
上面給出的兩種方法都可以正常工作,也完全符合C++標準。然而,標準還給出了另一種機制:導出模板。這種機制通常也被稱為C++模板的分離模型。
6.3.1 關鍵字 export
大體上講,關鍵字export的功能使用是非常簡單的:在一個文件裏面定義模板,並在模板的定義和(非定義的)聲明的前面加上關鍵字export。對於上面的例子改寫如下:

技術分享圖片
// basics/myfirst3.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP

// 模板聲明
export
template <typename T>
void print_typeof(T const&);

#endif  // MYFIRST_HPP
技術分享圖片

註:
1. 即使在模板定義不可見的條件下,被導出的模板也可以正常使用。換句話說,使用模板的位置和模板定義的位置可以在兩個不同的翻譯單元。
2. 在一個預處理文件內部(就是指在一個翻譯單元內部),我們只需要在第一個聲明前面標記export關鍵字就可以了,後面的重新聲明(也包括定義)會隱式地保留這個export特性,這也是我們不需要修改文件myfirst.cpp的原因所在。另一方面,在模板定義中提供一個冗余的export關鍵字也是可取的,因為這樣可以提高代碼的可讀性。
3. 實際上關鍵字export可以應用於函數模板、類模板的成員函數、成員函數模板和類模板的靜態數據成員。另外,它還可以用於類模板的聲明,這將意味著每個可導出的類成員(註意,是可導出的類成員,不可導出的依然不可導出)都被看做可導出實體,但類模板本身實際上卻沒有被導出(因此,類模板的定義仍然需要出現在頭文件中)。你仍然可以隱式或者顯式地定義內聯成員函數。然而,內聯函數卻是不可導出的,如下:

技術分享圖片
export template <typename T>
class MyClass
{
    public:
        void memfun1();     // 被導出的函數
        void memfun2(){ ... } // 隱式內聯不能被導出
        ... 
        void memfun3();      // 顯式內聯不能被導出
        ...
};
template <typename T>
inline void MyClass<T>::memfun3()   // 使用inline關鍵字,顯式內聯
{
    ...
}
技術分享圖片

4. export 關鍵字不能和inline關鍵字一起使用;如果用於模板的話,export要位於關鍵字template的前面,如下:

技術分享圖片
tempalte <typename T>
class Invalid
{
    public:
        export void wrong(T);       // ERROR, export 沒有位於template之前
};

export template <typename T>      // ERROR,同時使用export和inline
inline void Invalid<T>::wrong(T) { ... }
技術分享圖片

6.3.2 分離模型的限制

1. export特性為能像其他C++特性那樣廣為流傳;

2. export需要系統內部為“模板被實例化的位置和模板定義出現的位置”建立一些我們看不見的耦合;

3. 被導出的模板可能會導致出人意料的語義。

6.3.3 為分離模型做好準備 一個好的辦法就是:對於我們預先編寫的代碼,存在一個可以包含模型和分離模型之間互相切換的開關。我們使用預處理指示符來獲得這種特性,如下:

技術分享圖片
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// 如果定義了USE_EXPORT,就使用export
#if defined(USE_EXPORT)
#define EXPORT export
#else
#define EXPORT
#endif
// 模板聲明
EXPORT
template <typename T>
void print_typeof(T const&);

// 如果沒有定義USE_EXPORT,就包含模板定義
#if !defined(USE_EXPORT)
#include "myfirst.cpp"
#endif

#endif  // MYFIRST_HPP
技術分享圖片

6.4 模板和內聯

把短小函數聲明為內聯函數是提高運行效率所普遍采用的方法。inline修飾符表明的是一種實現:在函數的調用處使用函數體(即內容)直接進行內聯替換,它的效率要優於普通函數的調用機制(針對短小函數而言)。然而,標準並沒有強制編譯器實現這種“在調用處執行內聯替換”的機制,實際上,編譯器也會根據調用的上下文來決定是否進行替換(內聯並不是一種強制執行的機制)。

函數模板和內聯函數都可以被定義於多個翻譯單元中。通常,我們是通過下面的途徑來獲取這個實現:把定義放在一個頭文件中,而這個頭文件又被多個dot-C文件所包含(#include)。

這種實現會給我們這樣一個印象:函數模板缺省情況下是內聯的。然而,這種想法是不正確的。所以,如果你編寫需要被實現為內聯函數的函數模板,你仍然應該使用inline修飾符(除非這個函數由於是在類定義的內部進行定義的而已經被隱式內聯了)。

因此,對於許多不屬於類定義一部分的短小模板函數,你應該使用關鍵字inline來聲明它們。

6.5 預編譯頭文件

1. 當翻譯一個文件時,編譯器是從文件的開頭一直進行到文件的末端的;

2. 當處理文件中的每個標記(這些標記可能來自於#include的文件)時,編譯器會匹配它的內部狀態,包括添加入口點到符合表,從而在後面可以查找等。在這個過程中,編譯器還會在目標文件中生成代碼。

3. 預編譯頭文件機制主要依賴於下面的事實:我們可以使用某種方式來組織代碼,讓多個文件中前面的代碼都是相同的。充分利用預處理頭文件的關鍵之處在於:(盡可能地)確認許多文件開始處的相同代碼的最大行數。這意味著以#include指示符開始,同時意味著包含順序也相當重要;

4. 通常我們會直接創建一個名為std.hpp的頭文件,讓它包含所有的標準頭文件;

5. 管理預編譯頭文件的一種可取方法是:對預編譯頭文件進行分層,即根據頭文件的使用頻率和穩定性來進行分層。

6.6 調試模板
我們敘述的大多數編譯期錯誤就是由於違反了某些約束而產生的,我們把這些約束稱為語法約束;而對於其他約束,我們稱為語義約束。concept這個術語通常被用於表示:在模板庫中重復需求的約束集合。concept還可以形成體系:就是說,某個concept可以是其他concept的進一步細化(也稱為精華,更嚴格的約束),更精華的concept不但具備上層concept的各種約束,而且還增加了一些針對自身的約束。調試模板代碼的主要工作是判斷模板實現和模板定義中哪些concept被違反了。
更詳細的內容參見書籍。

------------------------------------------------------------------------------------------------------------
第7章 模板術語
7.1 “類模板”還是“模板類”
在C++中,類和聯合(union)都被稱為類類型(class type)。如果不加額外的限定,我們通常所說的“類(class)”是指:用關鍵字class或者struct引入的類類型。需要特別註意的一點就是:類類型包括聯合,而“類”不包括聯合。
7.2 實例化和特化

模板實例化是一個通過使用具體值替換模板實參,從模板產生出普通類、函數或者成員函數的過程。這個過程最後獲得的實體(譬如類、函數或成員函數)就是我們通常所說的特化。
然而,在C++中,實例化過程並不是產生特化的唯一方式。程序員可以使用其他機制來顯式地指定某個聲明,該聲明對模板參數進行特定的替換,從而產生特化,如:

技術分享圖片
template <typename T1, typename T2>     // 基本的類模板
class MyClass
{
    ...
};
template<>    // 顯式特化
class MyClass<std::string, float>
{
    ...
};
技術分享圖片

嚴格地說,上面就是我們通常所講的顯式特化(區別於實例化特化或者其他方式產生的特化)。

技術分享圖片
template <typename T>     // 基本的類模板
class MyClass<T, T>
{
    ...
};
template<typename T>    // 局部特化
class MyClass<bool, T>
{
    ...
};
技術分享圖片

另外,當談及(顯式或隱式)特化的時候,我們把普通模板稱為基本模板。

7.3 聲明和定義

1. 聲明是一種C++構造,它引入(或重新引入)一個名稱到某個C++作用域(scope)中;

2. 另外,對於宏定義和goto語句而言,即使它們都具有一個名稱,但它們卻不屬於聲明的範疇;

3. 如果已經確定了這種C++構造(即聲明)的細節,或者對於變量而言,已經為它分配了內存空間,那麽聲明就變成了定義;

4. 對於“類類型或者函數的”定義,這意味著必須提供一對花括號內部的實體;

5. 對於變量而言,進行初始化和不具有extern關鍵字的聲明都是定義。編譯器必須基於(哪個)特定實參對所包含的模板定義進行實例化

7.4 一處定義原則(ODR)
“C++語言的定義”在各個實體的重新聲明上面強加了一些約束,一處定義原則(或稱為ODR,one-definition rule)就是這些約束的全體。基本原則如下:
1. 和全局變量與靜態數據成員一樣,在整個程序中,非內聯函數和成員函數只能被定義一次;
2. 類類型和內聯函數在每個翻譯單元中最多只能被定義一次,如果存在多個翻譯單元,則其所有的定義都必須是等同的。
註:一個翻譯單元是指:預處理一個源文件所獲得的結果;就是說,它包括#include指示符(即所包含的頭文件)所包含的內容。
另外,我們所說的可鏈接實體指的是下面的實體:非內聯函數或者非內聯成員函數、全局變量或者靜態成員變量,還包括從模板產生的上述這些實體。

7.5 模板實參和模板參數
模板參數:位於模板聲明或定義內部,關鍵字template後面所列舉的名稱;
模板實參:用來替換模板參數的各個對象。
一個基本原則是:模板實參必須是一個可以在編譯期確定的模板實體或者值。如下:

template <typename T>       // 模板參數
class Dozen
{
    public:
        ArrayInClass<T, 12> contents;      // 模板實參
};

[轉]C++ template —— 模板基礎(一)