3. 類設計者工具
3.1 拷貝控制
- 五種函數
拷貝構造函數
拷貝賦值運算符
移動構造函數
移動賦值運算符
析構函數
拷貝和移動構造函數定義了當用同類型的另一個對象初始化本對象時做什么;
拷貝和移動賦值運算符定義了將一個對象賦予同類型的另一個對象時做什么。 - 拷貝構造函數
合成拷貝構造函數是編譯器定義的。
直接初始化:實際上要求編譯器使用普通的函數匹配來選擇與我們提供的參數最匹配的構造函數。
拷貝初始化:要去編譯器將右側運算對象拷貝到正在創建的對象中,需要的話還要進行類型轉換。
拷貝初始化發生的情況:
1)用=定義變量時
2)將一個對象作為實參傳遞給一個非引用類型的形參
3)從一個返回類型為非引用類型的函數返回一個對象
4)用花括號列表初始化一個數組中的元素或一個聚合類中的成員 - 容器調用insert或push時,會對其元素進行拷貝初始化。
emplace成員進行直接初始化。 - 拷貝構造函數被用來初始化非引用類型參數,這也解釋了為什么拷貝構造函數自己的參數時引用類型。如果不是引用類型,則調用不會成功,會陷入無限循環。
class Foo {
public:
Foo(); //默認構造函數
Foo(const Foo&); //拷貝構造函數
};
string dots(10, '-'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷貝初始化
string null_book = "9-999-999-9"; //拷貝初始化
string nines = string(100, '9'); //拷貝初始化
- 重載運算符本質上是函數,其名字由operator關鍵字后接表示要定義的運算符的符號組成。
- 拷貝賦值運算符
賦值運算符必須定義為成員函數,左側運算對象就綁定到隱式的this參數,右側運算對象作為顯示參數傳遞。
賦值運算符通常應該返回一個指向其左側運算對象的引用。
合成拷貝賦值運算符:編譯器定義的。
- 析構函數
析構函數有一個函數體和一個析構部分。首先執行函數體,然后銷毀成員。成員按初始化順序的逆序銷毀。
析構部分是隱式的。銷毀類類型需要執行成員自己的析構函數。內置類型沒有析構函數,因此銷毀內置類型成員什么也不需要做。
隱式銷毀一個內置指針類型不會delete它所指向的對象;當指向一個對象的引用或指針離開作用域時,析構函數不會執行。 - 析構函數什么時候調用——對象被銷毀時
1)變量在離開其作用域
2)對象被銷毀時,成員被銷毀
3)容器被銷毀時,其元素被銷毀
4)對于動態分配的對象,當對指向它的指針應用delete運算符時被銷毀
5)對于臨時對象,當創建它的完整表達式結束時被銷毀 - 合成析構函數
成員是在析構函數體之后隱含的析構階段中被銷毀的。
- 三/五法則
拷貝構造函數、拷貝賦值運算符、構造函數、移動構造函數、移動賦值運算符被看作一個整體。
1)需要析構函數的類也需要拷貝和賦值操作
2)需要拷貝操作的類也需要賦值操作,反之亦然
例子:一個類為每個對象分配一個獨有的、唯一的序號。拷貝構造函數和拷貝賦值運算符需要手動定義,以避免將序號賦予目的對象。
只能對編譯器可以合成的默認構造函數或拷貝控制成員使用=default。
class HasPtr{
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) {}
~HasPtr() {delete ps;}
//需要定義拷貝構造函數和拷貝賦值運算符
//因為合成的只是進行值拷貝,也就是指針拷貝
//會導致多個HasPtr對象指向相同的內存。
};
- 阻止拷貝
iostream類阻止了拷貝,以避免多個對象寫入或讀取相同的IO緩存。
通過將拷貝構造函數和拷貝賦值運算符定義為刪除函數來阻止拷貝。
刪除函數:雖然進行了聲明,但不能以任何方式使用。
在函數的參數列表后面加上=delete即可。
如果一個類有數據成員不能默認構造、拷貝、復制或銷毀,則對應的成員函數將被定義為刪除的。(包括const成員和引用)
- 通常,管理類外資源的類必須定義拷貝控制成員,兩種定義拷貝操作的方法:
1)行為像值的類
標準庫容器和string類的行為像一個值。
2)行為像指針的類
shared_ptr類提供類似指針的行為。 - IO類型和unique_ptr不允許拷貝或賦值,因此它們的行為既不像值也不像指針。
- 行為像值的類
每個對象都應該擁有一份自己的拷貝。
賦值運算符通常組合了析構函數和構造函數的操作。賦值操作會銷毀左側運算對象的資源,會從右側運算對象拷貝數據。
1)保證一個對象賦予它自身,也保證正確
2)異常安全——當異常發生時,能將左側運算對象置于有意義的狀態(下面的貌似不算絕對的異常安全)
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) {}
HasPtr(const HasPtr &p):
ps(new std::string(*p.ps)), i(p.i) {}
HasPtr& operator=(const HasPtr &);
~HasPtr() {delete ps;}
private:
std::string *ps;
int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
auto newp = new string(*rhs.ps);
delete ps; //不算絕對的異常安全,應該使用后面的swap
ps = newp;
i = rhs.i;
return *this;
}
-
行為像指針的類
析構函數不能單方面地釋放關聯的string。只有當最后一個指向string的HasPtr銷毀時,它才可以釋放string。可以使用shared_ptr,也可以設計自己的引用計數。
計數器必須是所有對象共有的,所以可以保存在一塊動態內存中,所有對象共享。
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
HasPtr(const HasPtr &p) :
ps(p.ps), i(p.i), use(p.use) {++*use;}
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use;
};
HasPtr::~HasPtr() {
if (--*use == 0) {
delete ps;
delete use;
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
++*rhs.use;
if (--*use == 0) {
delete ps;
delete use;
}
ps = rhs.ps
i = rhs.i;
use = rhs.use;
return *this;
}
- 交換操作
每個swap調用都應該是未加限定的,而不是std::swap。using聲明(using std::swap)不會隱藏自定義版本的swap聲明。
定義了swap的類通常用swap來定義其賦值運算符。這些運算符使用了拷貝并交換技術。
使用拷貝和交換的賦值運算符時異常安全的,且能正確處理自賦值。唯一肯呢個拋出異常的是拷貝構造函數中的new,若發生異常,也是改變左側對象之前發生的。
HasPtr& HasPtr::operator=(HasPtr rhs) { //傳值的方式
swap(*this, rhs);
return *this; //rhs被銷毀
}
- 拷貝控制的應用場合
1)資源管理
2)簿記操作或其他操作Message和Folder
- 動態內存管理類
某些類需要在運行時分配可變大小的內存空間:
1)使用標準容器
2)自己進行內存分配,一般必須定義自己的拷貝控制成員來管理分配的內存。StrVec
- 對象移動
在某些情況下,對象拷貝后就立即被銷毀了。此時,移動而非拷貝對象會大幅度提升性能。
使用移動而不是拷貝的另一個原因源于IO類或unique_ptr類。這些類包含不能被共享的資源(如指針或IO緩沖),因此,這些類型的對象不能拷貝但可以移動。 - 右值引用
必須綁定到右值的引用,通過&&來獲得。
右值引用有一個重要性質——只能綁定到一個將要銷毀的對象。因此,可以將一個右值引用的資源“移動”到另一個對象中。
右值引用可以綁定到要求轉換的表達式、字面常量或返回右值的表達式,但不能直接綁定到一個左值上。
1)返回左值的例子:返回左值的函數、賦值、下標、解引用、前置遞增/遞減運算符
2)返回右值的例子:返回非引用的函數、算術、關系、位、后置遞增/遞減運算符(可將const左值引用、右值綁定到這些類型)
左值有持久的狀態,右值要么是字面常量,要么是在表達式求值過程中創建的臨時對象。
可以通過move獲得綁定到左值上的右值引用。可以銷毀一個移后源對象,可以賦予其新值,但不能使用一個移后源對象的值。
int &&rr1 = 42;
錯 int &&rr2 = rr1;
int &&rr3 = std::move(rr1);
- 移動構造函數
移動操作通常不分配任何資源。一旦資源完成移動,源對象不能再指向被移動的資源,這些資源的所有權已經歸屬新創建的對象。
一個移動操作不拋出異常,是因為兩個相互關聯的事實:首先,雖然移動操作通常不拋出異常,但拋出異常是允許的;其次,標準庫容器能對異常發生時起自身的行為提供保障。例如vector調用push_back發生異常時,vector自身不會發生改變。
除非vector知道元素類型的移動構造函數不會拋出異常,否則在重新分配內存的過程中,它就必須使用拷貝構造函數而不是移動構造含。
(這個noexcept解釋的不清不楚!!)
StrVec::StrVec(StrVec &&s) noexcept : //移動操作不應拋出任何異常
elements(s.elements), first_free(s.first_free), cap(s.cap) {
s.elements = s.first_free = s.cap = null_ptr;
}
- 移動賦值運算符
必須正確處理自賦值
StrVec &StrVec::operator=(StrVec &&rhs) noexcept{
if (this != &rhs) {
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
- 移后源對象必須可析構
移動操作還必須保證對象仍然是有效的,也即可以安全地為其賦予新值或者可以安全地使用而不依賴其當前值。
移動操作對移后源對象中留下的值沒有任何要求。程序不應該依賴于移后源對象的數據。 - 合成的移動操作
不聲明拷貝構造函數或拷貝賦值運算符,編譯器會合成,要么定義為逐成員拷貝,要么定義為對象賦值,要么定義為刪除的函數。
如果一個類定義了拷貝構造函數、拷貝賦值運算符或者析構函數,編譯器不會為它合成移動構造函數和移動賦值運算符。
只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static數據成員都可以移動時,編譯器才會為它合成移動構造函數和移動賦值運算符。編譯器可以移動內置類型的成員,若是類類型,且該類有對應的移動操作,編譯器也能移動這個成員。 -
與拷貝操作不同,移動操作不會隱式地定義為刪除的函數。若顯式地要求編譯器生成=default移動操作,且編譯器不能移動所有成員,則編譯器會將移動操作定義為刪除的函數。
合成的移動操作定義為刪除的函數遵循的原則:
- 如果類定義了一個移動構造函數或一個移動賦值運算符,則該類的合成拷貝構造函數和拷貝賦值運算符會被定義為刪除的。因此該類必須定義自己的拷貝操作。
- 移動右值,拷貝左值,但如果沒有移動構造函數,右值也被拷貝
拷貝構造函數滿足對應的移動構造函數的要求,拷貝賦值運算符合移動賦值運算符類型。 - 拷貝并交換賦值運算符和移動操作
賦值運算符既是移動賦值運算符,也是拷貝賦值運算符
class HasPtr {
public:
HasPtr(HasPtr &&p) noexcept: ps(p.ps), i(p.i) {p.ps = 0;}
HasPtr& operator=(HasPtr rhs)
{swap(*this, rhs); return *this;}
};
hp = hp2; //拷貝構造函數來拷貝
hp = std::move(hp3); // 移動構造函數來拷貝
- 總結性的建議:
某些類必須定義拷貝構造函數、拷貝賦值運算符和析構函數才能正確工作。這些類通常擁有一個資源,而拷貝成員必須拷貝此資源。一般說來,拷貝一個資源會導致一些額外的開銷。在這種拷貝并非必要的情況下,定義移動構造函數和移動賦值運算符的類就可以避免此問題。 - 移動迭代器適配器
移動迭代器的解引用運算符生成一個右值引用。make_move_iterator將一個普通迭代器轉換為一個移動迭代器。
由于移動一個對象可能銷毀掉源對象,因此你只有在確信算法在為一個元素賦值或將其傳遞給一個用戶定義的函數后不再訪問它時,才能將移動迭代器傳遞給算法。 - 只有確信移后源對象沒有其他用戶時,才使用移動操作。
- 區分移動和拷貝的重載函數通常有一個版本接受const T&,另一個版本接受T&&
- 指出this的左值/右值屬性的方式與定義const成員函數相同,在參數列表后放置一個引用限定符。引用限定符可以是&或&&,分別指出this可以指向一個左值或右值。
class Foo{
public:
Foo &operator=(const Foo&) &; //只能向可修改的左值賦值
};
Foo &Foo::operator=(const Foo &rhs) & {
...
return *this;
}
- 引用限定符可以區分重載版本。若定義了兩個或兩個以上具有相同名字和相同參數列表的成員函數,就必須對所有的函數都加上引用限定符,或者所有都不加。
class Foo {
public:
Foo sorted() &&; //可用于可改變的右值
Foo sorted() const &; //可用于任何類型的Foo
private:
vector<int> data;
};
Foo Foo::sorted() && {
sort(data.begin(), data.end());
return *this;
}
//本對象時const或是一個左值,不能對其進行原址排序
Foo Foo::sorted() const &{
Foo ret(*this);
sort(ret.data.begin(), ret.data.end());
return ret;
}
Foo &retFoo(); //返回一個引用;retFoo調用是一個左值
Foo retVal(); //返回一個值;retVal調用是一個右值
retVal().sorted(); //調用Foo::sorted() &&
retFoo().sorted(); //調用Foo::sorted() const &
3.2 重載運算符與類型轉換
- 除了operator()之外,其他重載運算符不能含有默認實參
-
如果一個運算符函數是成員函數,則它的第一個(左側)運算對象綁定到隱式的this指針上,因此,成員運算符函數的(顯式)參數數量比運算符的運算對象總數少一個。
- (, & && ||)某些運算符指定了運算對象求值的順序,因為使用重載的運算符本質上是一次函數調用,所以這些關于運算對象求值順序的規則無法應用到重載的運算符上。包括:邏輯與、或和逗號,兩個對象總是會被求值。
逗號和取地址運算符,C++已經定義了這兩種運算符用于類類型對象時的特殊含義,所以不應重載。 -
重載時,使用與內置類型一直的含義
當在內置的運算符和自己的操作之間存在邏輯映射關系時,運算符重載的效果最好。只有當操作的含義對于用戶來說清晰明了才使用運算符。
-
將運算符定義為成員函數還是普通的非成員函數做出的抉擇:
- 重載輸出運算符<<
<<的第一個形參是非常量ostream對象的引用:非常量,向流寫入內容會改變其狀態;引用,無法直接復制一個ostream對象。
輸出運算符應該主要打印對象的內容而非控制格式,輸出運算符不應該打印換行符。
輸入輸出運算符必須是非成員函數。
ostream &operator<<(ostream &os, const Sales_data &item){
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
-
重載輸入運算符>>
輸入運算符必須處理輸入可能失敗的情況,輸出不需要。
出錯的情況:
istream &operator>>(istream &is, Sales_data &item) {
double price;
is >> item.bookNo >> item.units_soled >> price;
if is) {
item.revenue = item.units_sold * price;
} else { //處理出錯的情況
item = Sales_data();
}
return is;
}
- 通常,將算術和關系運算符定義成非成員函數以允許對左側或右側的運算對象進行轉換。因為這些運算符一般不需要改變對象的狀態,所以形參都是常量引用。
如果類同時定義了算術運算符和相關的復合賦值運算符,則通常情況下應該使用復合賦值來實現算術運算符。 -
相等運算符的設計準則
- 關系運算符
1)定義順序關系,令其與關聯容器中對關鍵字的要求一致
2)如果類同時也含有==運算符,則定義一種關系令其與==保持一致。特別是,如果兩個對象是!=的,那么一個對象應該<另外一個。
如果存在唯一一種邏輯可靠的<定義,則應該考慮為這個類定義<運算符。如果類同時還包含==,則當且僅當<的定義和==產生的結果一致時才定義<運算符。
- 賦值運算符——必須定義為成員函數
在拷貝賦值和移動賦值運算符外,vector還定義了第三種賦值運算符,即接受花括號內的元素列表作為參數的賦值運算符。
initializer_list<string>確保il與this所指的不是同一個對象,所以無需檢查對象向自身的賦值。
StrVec &StrVec::operator=(initializer_list<string> il) {
auto data = alloc_n_copy(il.begin(), il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
- 復合賦值運算符
不非得是類的成員,不過傾向于定義為成員函數。
- 下標運算符——必須是成員函數
最好同時定義下標運算符的常量和非常量版本,當作用于一個常量對象時,下標運算符返回常量引用以確保不會給返回的對象賦值。
class StrVec{
public:
std::string& operator[](std::size_t n) {
return elements[n];
}
const std::string& operator[](std::size_t n) const {
return elements[n];
}
private:
std::string *elements;
};
- 遞增和遞減運算符——建議定義成類的成員
- 定義前置遞增/遞減運算符
前置運算符應該返回遞增或遞減后對象的引用
class StrBlobPtr {
public:
StrBlobPtr& operator++();
StrBlobPtr& operator--();
};
- 區分前置和后置運算符
后置接受一個額外的(不被使用)int類型的形參。
為了與內置版本保持一致,后置運算符應該返回對象的原值,返回的形式是一個值,而非引用。
class StrBlobPtr {
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
};
- 編譯器gcc是怎樣區分前置和后置版本的遞增和遞減符號???
-
成員訪問運算符
->運算符必須是類的成員,解引用通常也是類的成員。
point->mem執行過程如下:
也即:重載的->運算符必須返回類的指針或者自定義了
class StrBlobPtr {
public:
std::string& operator*() const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
std::string* operator->() const {
return & this->operator*();
}
};
- 不是特別理解->的定義,查看gcc編譯器是怎么處理的???
- 函數調用運算符
必須是成員函數。一個類可以定義多個不同版本的調用運算符。
如果類定義了調用運算符,則該類的對象稱作函數對象。
函數對象類可以含有狀態。
函數對象常常作為泛型算法的實參
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' '):
os(o), sep(c) {}
void operator()(const string &s) const {os << s << sep; }
private:
ostream &os;
char sep;
};
PrintString printer;
printer(s);
PrintString errors(cerr, '\n');
errors(s);
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
- lambda是函數對象
編譯器會將lambda表達式翻譯成一個未命名類的未命名對象,在lambda表達式產生的類中含有一個重載的函數調用運算符。
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) {return a.size() < b.size();});
//lambda類似于下面這個類的一個未命名對象
class ShorterString {
public:
bool operator() (const string &s1, const string &s2) const {
return s1.size() < s2.size();
}
};
stable_sort(words.begin(), words.end(), ShorterString());
- 表示lambda捕獲行為的類
lambda表達式產生的類不含默認構造函數、賦值運算符及默認析構函數;它是否含有默認的拷貝/移動構造函數則通常要視捕獲的數據成員類型而定。
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
{return a.size() >= sz;});
class SizeComp {
public:
SizeComp(size_t n): sz(n) {}
bool operator() (const string &s) const
{return s.size() >= sz;}
private:
size_t sz;
};
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
-
標準庫定義的函數對象
表示運算符的函數對象類常用來替換算法中的默認運算符。
sort(svec.begin(), svec.end(), greater<string>());
- C++中有幾種可調用的對象
函數
函數指針
lambda表達式
bind創建的對象
重載了函數調用運算符的類 - 可調用對象也有類型,lambda有它自己唯一的(未命名)類類型,函數及函數指針的類型則由其返回值類型和實參類型決定。
兩個不同類型的可調用對象可能共享同一種調用形式。 -
標準庫的function類型
function可將不同類型的可調用對象都存儲在同一個function類型中。
function<int(int, int)> f1 =add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = [](int i, int j) {return i * j;};
map<string, function<int(int, int)>> binops; //函數表
- 重載函數與function
不能直接將重載函數的名字放入function類型的對象中。
二義性問題解決方法:
1)存儲函數指針
2)使用lambda來消除二義性
- 用戶定義的類類型轉換
1)轉換構造函數:有一個實參調用的非顯式構造函數定義了一種隱式的類型轉換
2)類型轉換運算符 - 類型轉換運算符——必須定義成類的成員函數
operator type() const;
其中type表示某種類型。類型需要能作為返回類型,因此不允許數組、函數類型,但是允許指針或者引用類型。
不能聲明返回類型,形成列表必須為空。通常應該是const,不改變待轉換對象的內容。
如果在類類型和轉換類型之間不存在明顯的映射關系,則這樣的類型轉換可能具有誤導性。
class SmallInt {
public:
SmallInt(int i = 0) : val(i) { //int轉為SmallInt對象
if (i < 0 || i > 255) {
throw std::out_of_range("Bad SmallInt value");
}
operator int() const {return val;} //定義SmallInt對象轉換成int
}
private:
std::size_t val;
};
si = 4; // 4轉換成SmallInt
si + 3; // si轉換成int
- 在實踐中,類很少提供類型轉換運算符。例外情況:定義向bool的類型轉換還是比較普遍的現象。
- 顯式的類型轉換運算符
例外:如果表達式被用作條件,則編譯器會將顯式的類型轉換自動應用于它。
class SmallInt {
public:
explicit operator int() const {return val;} //定義SmallInt對象轉換成int
}
};
si = 4; // 4轉換成SmallInt
si + 3; // 錯誤
static_cast<int>(si) + 3; // 正確
- 無論什么時候在條件中使用流對象,都會使用IO類型定義的operator bool。
下面語句讀入數據到value并返回cin,cin被istream operator bool類型轉換函數隱式地執行了轉換,如果cin的條件狀態是good,則該函數返回為真,否則返回假。
向bool的類型轉換通常用在條件部分,因此operator bool一般定義成explicit。
while (std::cin >> value)
- 避免有二義性的類型轉換
1)相同的類型轉換B->A:一種使用A的以B為參數的構造函數;一種使用B的類型轉換運算符
2)定義了多個轉換規則,而這些類型本身可以通過其他類型轉換聯系在一起。典型的例子是算術類型。
通常類型下,不要為類定義相同的類型轉換,也不要在類中定義兩個及兩個以上轉換源或轉換目標是算術類型的轉換。 - 總之,除了顯式地向bool類型的轉換之外,我們應該盡量避免定義類型轉換函數并盡可能地限制那些“顯然正確”的非顯示構造函數。
- 重載函數與轉換構造函數
如果在調用重載函數時需要使用構造函數或者強制類型轉換來改變實參的類型,則這通常意味著程序的設計存在不足
struct C {
C(int);
};
struct D {
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10); //二義性錯誤
- 重載函數與用戶定義的類型轉換
在調用重載函數時,如果需要額外的標準類型轉換,則該轉換的級別只有當所有可行函數都請求同一個用戶定義的類型轉換時才有用。如果所需的用戶定義的類型轉換不止一個,則該調用具有二義性。
struct E {
E(double);
};
void manip(const C&);
void manip(const E&);
manip(10); //二義性錯誤
- 函數匹配與重載運算符
a sym b可能是:
1)a.operatorsym(b); //成員函數
2)operatorsym(a,b); //普通函數
表達式中運算符的候選函數集既應該包括成員函數,也應該包括非成員函數。
如果對同一個類既提供了轉換目標是算術類型的類型轉換,也提供了重載的運算符,則將會遇到重載運算符與內置運算符的二義性問題。
3.3 面向對象程序設計
- 面向對象程序設計基于三個基本概念:數據抽象、繼承和動態綁定。
通過使用數據抽象,將類的接口和實現分離;
使用繼承,可以定義相似的類型并對其相似關系建模;
使用動態綁定,可以在一定程度上忽略相似類型的區別,而以統一的方式使用它們的對象。 - 對于某些函數,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數聲明成虛函數。
派生類使用類派生列表之處它是從哪個基類繼承而來。
派生類必須在其內部對所有重新定義的虛函數進行聲明。新標準可以通過在形參列表后加override關鍵字來顯式地標明改寫。 - 函數的運行版本由實參決定,即在運行時選擇函數的版本,所以動態綁定有時又被稱作運行時綁定。
- 定義基類和派生類
基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
派生類需要對類型相關的操作(虛函數)提供自己的新定義以覆蓋(override)從基類繼承而來的舊定義,派生類中該函數隱式地也是虛函數。
任何構造函數之外的非靜態函數都可以是虛函數。virtual只能出現在類內部的聲明語句之前而不能用于類外部的函數定義。
成員函數如果沒有被聲明成虛函數,則其解析過程發生在編譯時而非運行時。
派生是公有的,則能將公有派生類型的對象綁定到基類的引用或指針上。 - 一個派生類對象包含多個組成部分:一個含有派生類自己定義的(非靜態)成員的子對象,以及一個與該派生類繼承的基類對于的子對象。
- 派生類必須使用基類的構造函數來初始化它的基類部分。
首先初始化基類的部分,然后按照聲明的順序依次初始化派生類的成員。 - 每個類負責定義各自的接口。派生類應該遵循基類的接口,并且通過調用基類的構造函數來初始化那些從基類中繼承而來的成員。
- 如果想將某個類用作基類,則該類必須已經定義而非僅僅聲明
- 防止繼承的發生,可以在類名后加一個關鍵字final,該類將不能作為基類。
- 靜態類型:編譯時已知,它是變量聲明時的類型或表達式生成的類型
動態類型:變量或表達式表示的內存中的對象的類型,直到運行時才知道。
如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態類型一直。
基類的指針或引用可以綁定到派生類對象,智能指針類也支持派生類向基類的類型轉換。 - 因為一個基類對象可能是派生類對象的一部分,也可能不是,所以不存在從基類向派生類的自動類型轉換。
即使一個基類指針或引用綁定在一個派生類對象上,也不能執行從基類向派生類的轉換。因為編譯器只能通過檢查指針或引用的靜態類型來推斷該轉換是否合法。基類中含有虛函數,可以通過dynamic_cast請求類型轉換;如果已知轉換是正常,可以使用static_cast。 - 在派生類和基類的對象之間不存在類型轉換。
當用一個派生類對象為一個基類初始化或賦值時,只有該派生類對象中的基類部分會被拷貝、移動或賦值,它的派生類部分將被忽略掉。
Bulk_quote bulk; //派生類對象
Quote item(bulk); //調用基類Quote::Quote(const Quote&)構造函數
item = bulk; //調用基類Quote::operator=(const Quote&)
- 總結:
1)派生類向基類的類型轉換只對指針或引用類型有效
2)基類向派生類不存在隱式類型轉換
3)和任何其他成員一樣,派生類向基類的類型轉換也可能會由于訪問受限而變得不可行
4)可以將派生類對象拷貝、移動或賦值給一個基類對象,這些操作只處理派生類對象的基類部分
- 派生類中的虛函數的返回類型必須與基類函數匹配。
該規則存在一個例外,當類的虛函數返回類型是類本身的指針或引用時,如D由B派生得到,則基類的虛函數可以返回B而派生類的對應函數可以返回D,不過這樣的返回類型要求從D到B的類型轉換是可訪問的。 - override如果標記了某個函數,但該函數并沒有覆蓋已存在的虛函數,此時編譯器將報錯。
將某個函數指定為final,則之后任何嘗試覆蓋該函數的操作都將引發錯誤。 - 如果虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致。
- 回避虛函數機制,可以通過使用作用域運算符實現這一目的。
需要回避虛函數機制的地方?通常是當一個派生類的虛函數調用它覆蓋的基類的虛函數版本時。
- 純虛函數,在函數體的位置=0,一個純虛函數無需定義,也可以定義在類的外部,不能在類的內部進行定義。
- 含有純虛函數的類是抽象基類。抽象基類負責定義接口,而后續的其他類可以覆蓋該接口。不能(直接)創建抽象基類的對象。
后續的派生類必須覆蓋純虛函數,否則仍將是抽象基類。
- protected的一個重要性質:派生類的成員或友元只能通過派生類的對象來訪問基類的受保護成員。派生類對于一個基類對象中的受保護成員沒有任何訪問權限。
這個也是為了維持封裝(數據抽象)的需要,否則頂一個繼承子類就可以規避掉protected的屬性,封裝形同虛設。 - 派生訪問說明符對于派生類的成員(及友元)能否訪問其直接基類的成員沒有影響。對于基類的成員訪問權限只與基類中的訪問說明符有關。
派生訪問說明符的目的是控制派生類用戶(包括派生類的派生類在內)對于基類成員的訪問權限。 -
派生類向基類轉換的可訪問性
- 基類應該將其接口聲明為公有的,將屬于實現的部分分為兩組:一組可供派生類訪問,聲明為保護的;另一組只能由基類及基類的友元訪問,聲明為私有的。
- 友元關系不能傳遞,也不能繼承。
每個類負責控制自己的成員的訪問權限,包括基類內嵌在其派生類對象中的情況。 - 派生類只能為那些它可以訪問的名字提供using聲明,而這可以改變該成員的可訪問性(類的實現者、派生類、普通用戶)。
- 派生類的作用域位于基類作用域之內
- 一個對象、引用或指針的靜態類型決定了該對象的哪些成員是可見的。即使靜態類型與動態類型不一致。
這個可以用作用域嵌套來解釋,因為基類的指針或引用是從基類的作用域開始搜索的,無法訪問內嵌在其里面的派生類的作用域。 - 派生類的成員將隱藏同名的基類成員,同理可用作用域嵌套來解釋。
可以通過作用域運算符來使用一個被隱藏的基類成員。 -
函數調用的解析過程,假定調用p->mem()或obj.mem(),則依次執行以下四個步驟:
- 聲明在內層作用域的函數并不會重載聲明在外層作用域的函數。這是因為名字查找先于類型檢查。
因此若派生類的成員與基類的某個成員同名,則派生類將在其作用域內隱藏該基類的成員。 - 如果派生類希望所有的重載版本對于它都是可見的,則要么覆蓋所有版本,要么一個也不覆蓋。
using聲明語句指定一個名字而不指定形參列表,基類的using聲明語句就可以把該函數的所有重載實例添加到派生類作用域中。派生類只需要定義其特有的函數就可以了。
class Base {
public:
virtual int fcn();
};
clas D1 : public Base{
public:
int fcn(int); //該fcn不是虛函數,隱藏基類的fcn,這里應該不算隱藏吧!
virtual void f2();
};
class D2 : public D1{
public:
int fcn(int); //隱藏D1::fcn(int)
int fcn(); //覆蓋了Base的虛函數fcn
void f2(); //覆蓋D1的虛函數f2
};
Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虛調用,Base::fcn
bp2->fcn();//虛調用,Base::fcn,不是照樣可以調用么,不算隱藏
bp3->fcn();//虛調用,D2::fcn
D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
bp2->f2(); //錯誤,基類指針不能訪問派生類的成員
d1p->f2(); //虛調用,D1::f2()
d2p->f2(); //虛調用,D2::f2()
Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p3 = &d2obj;
p1->fcn(42); //錯誤,沒有匹配的函數
p2->fcn(42); //靜態綁定,調用D1::fcn(int)
p3->fcn(42); //靜態綁定,調用D2::fcn(int)
- 虛析構函數
基類定義一個虛析構函數,這樣就能動態分配繼承體系中的對象。
delete指針時,將析構函數定義為虛函數以確保執行正確版本的析構函數。
如果基類的析構函數不是虛函數,則delete一個指向派生類對象的基類指針將產生未定義的行為。
一個基類總是需要析構函數(內容為空),但是無法推斷該基類還需要賦值運算符或拷貝構造函數。
虛析構函數將阻止合成移動操作。 -
合成拷貝控制與繼承
某些定義基類的方式也可能導致派生類成員成為被刪除的函數:
如果定義了拷貝構造函數,編譯器不會合成移動構造函數。
一旦基類定義了移動操作,必須同時顯式地定義拷貝操作。派生類將自動獲得合成的移動操作。 - 派生類的拷貝控制成員
當派生類定義了拷貝或移動操作時,該操作負責拷貝或移動包括基類部分成員在內的整個對象。
當為派生類定義拷貝或移動構造函數時,通常在初始值列表中顯式使用對應的基類構造函數(或移動函數)初始化對象的基類部分。
派生類賦值運算符也許要顯式地為其基類部分賦值。
析構函數與構造、賦值不同,派生類析構函數只負責銷毀由派生類自己分配的資源,基類部分是隱式銷毀的。
如果構造函數或析構函數調用了某個虛函數,則應該執行與構造函數或析構函數所屬類型相對應的虛函數版本。 - 繼承的構造函數
派生類繼承基類構造函數的方式是提供注明基類名的using聲明語句。如果派生類含有自己的數據成員,這些成員將被默認初始化。
using聲明不會改變構造函數的訪問級別,以及explicit、constexpr屬性。
當一個基類構造函數含有默認實參,將獲得多個構造函數,其中每個構造函數額外獲得分別省略一個含有默認實參的形參。
派生類會繼承所有構造函數,除了:
1)派生類重新定義了某個相同類型的構造函數
2)默認、拷貝和移動構造函數不會被繼承。
- 容器與繼承
當在容器中存放具有繼承關系的對象時,實際上存放的通常是基類的指針(最好用智能指針)。 - 單詞查詢的面向對象的解決方案——一個很經典的方法
3.4 模板與泛型編程
- 定義模板
<>是模板參數列表,該列表不能為空。
編譯器通常用函數實參來推斷出模板實參,也即實例化一個特定版本的函數。
模板類型參數的每個參數前都要有typename或class。
非類型參數表示一個值,指定特定的類型名,調用時,這些值必須是常量表達式。綁定到指針或引用非類型參數的實參必須具有靜態生存期。
函數模板可以聲明為inline或constexpr。
template<typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
template <typename T> inline T min(const T&, const T&);
- 編寫泛型代碼的兩個重要原則
1)模板中的函數參數時const 的引用
2)函數體中的條件判斷僅使用<比較運算
模板程序應該盡量減少對實參類型的要求。 - 只有當實例化出模板的一個特定版本時,編譯器才會生成代碼。
為了生成一個實例化版本,編譯器需要掌握函數模板或類模板成員函數的定義。因此,與非模板代碼不同,模板的頭文件通常既包括聲明也包括定義。 - 類模板
編譯器不能為類模板推斷模板參數類型。
定義在類模板之外的成員函數必須以關鍵字template開始,接類模板參數列表。
默認情況下,一個類模板的成員函數只有當程序用到它時才進行實例化。
在類模板自己的作用域中,可以直接使用模板名而不提供實參。 - 類模板與友元
1)一對一關系
友元的聲明用Blob的模板形參作為它們自己的模板實參,因此,友元關系被限定在用相同類型實例化的Blob與BlobPtr相對運算符之間。
2)通用和特定的模板友好關系
3)模板自己的類型參數可以是友元 - 可以定義一個typedef來引用實例化的類
可以為類模板定義一個類型別名,并且可以固定一個或多個模板參數。 - 類模板的static成員函數同樣只在使用時才會實例化。
- 在模板內不能重用模板參數名
錯誤 template <typename V, typename V>
- 默認情況下,C++假定通過作用域運算符訪問的名字不是類型。
若希望使用一個模板類型參數的類型程序,必須使用typename(不能使用class)顯式告訴編譯器該名字是一個類型。
template <typename T>
typename T::value_type top(const T& c) {
}
- 新標準中可以為函數和類模板提供默認實參
template <typename T, typename F = less<T>>
int compare (const T &v1, const T &v2, F f = F() ) { //f是類型F的一個默認初始化對象
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
希望使用默認實參時,在類模板名之后跟一個空尖括號對即可。
成員模板
一個類(普通類或是類模板)可以包含本身是模板的成員函數,成員模板不能是虛函數。
1)普通類的成員模板
類似unique_ptr所使用的默認刪除器類型。由于刪除器適用于任何類型,所以將調用運算符定義為一個模板。
2)類模板的成員模板
此時,類和成員各自有自己的獨立的模板參數。
為了實例化一個類模板的成員模板,必須同時提供類和函數模板的實參。與普通函數模板一樣,編譯器通常根據傳遞給成員模板的函數實參來推斷它的模板實參。控制實例化
模板使用時才實例化意味著,相同的實例可能出現在多個對象文件中,額外開銷可能非常嚴重。
可以通過顯式實例化來避免這種開銷。
當編譯器遇到extern模板聲明時,不會在本文件中生成實例化代碼。對于一個給定的實例化版本,可能有多個extern聲明,但必須只有一個定義。
extern聲明必須出現在任何使用此實例化版本的代碼之前。
實例化定義會實例化所有成員,與處理類摩拜的普通實例化不同(使用時才實例化)。
extern template declaration; // 實例化聲明
template declaration; //實例化定義
- 效率與靈活性
1)在運行時綁定刪除器——shared_ptr
2)在編譯時綁定刪除器——unique_ptr
unique_ptr有兩個模板參數,一個表示它所管理的指針,另一個表示刪除器的類型。
通過在編譯時綁定刪除器,unique_ptr避免了間接調用刪除器的運行時開銷。通過在運行時綁定刪除器,shared_ptr使用戶重載刪除器更為方便。
-
類型轉換與模板類型參數
頂層const會被忽略。
能在調用中應用于函數模板的類型轉換:
其他類型轉換(算術轉換、派生類向基類轉換、用戶定義的轉換)都不能應用于函數模板。
數組的大小不同,因此是不同類型。轉換為指針后一樣,但是如果是引用,則數組不會轉換為指針。
template <typename T> T fobj(T, T); //實參被拷貝
template <typename T> T fref(const T&, const T&); //引用
int a[10], b[42];
fobj(a, b); //調用f(int *, int *)
fref(a, b); //錯誤,數組類型不匹配
- 一個模板類型參數可以用作多個函數形參的類型,傳遞給這些形參的實參必須具有相同的類型。如果推斷出的類型不匹配,則調用就是錯誤的。
如果希望允許對函數實參進行正常類型轉換,可以將函數模板定義為多個類型參數。
long lng;
compare (lng, 1024);//錯誤:不能實例化compare(long, int)
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2);
- 如果函數參數不是模板參數,則對實參進行正常類型轉換
- 指定顯式模板實參
如下,沒有任何函數實參的類型可以推斷T1的類型,每次調用sum時必須為T1提供一個顯示模板實參。
顯示模板實參按由左至右與對應的模板參數匹配。只有尾部(最右)參數的顯式模板實參才可以忽略,前提是他們可以從函數參數推斷出來。
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
- 正常類型轉換應用于顯式指定的實參
long lng;
compare(lng, 1024); //錯誤
compare <long>(lng, 1024); //正確,實例化compare(long, long)
compare<int>(lng, 1024); //正確實例化compare(int, int)
- 尾置返回類型與類型轉換
接受序列的一對迭代器和返回序列中一個元素的引用,在編譯器遇到函數的參數列表之前,beg是不存在,因此使用尾置返回類型。
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg){
return *beg;
}
-
進行類型轉換的標準庫模板類——頭文件type_traits
迭代器操作只會生成元素的引用,為了獲得元素類型(值),可以使用標準庫的類型轉換模板。
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type{
return *beg;//返回序列中的一個元素的拷貝
}
//第二個typename告知編譯器,type是一個類型,前面有該知識點。
*函數指針和實參推斷
函數指針初始化或賦值為函數模板,編譯器用指針的類型來推斷模板實參。
如果函數指針是重載版本,則無法確定模板實參的唯一類型,會出現錯誤。此時需要用顯式模板實參。
- 模板實參推斷和引用
1)從左值引用函數參數推斷類型
2)從右值引用函數參數推斷類型
3)引用折疊和右值引用參數
通常不能將一個右值引用綁定到一個左值上。
兩個例外規則,允許這種綁定,這兩個例外規則是move標準庫設施正確工作的基礎。
例外一、影響右值引用參數的推斷如何進行。將一個左值傳遞給函數的右值引用參數,且此右值引用指向模板類型參數(如T&&)時,編譯器推斷模板類型參數為實參的左值引用類型。此時意味著函數參數是一個類型int&的右值引用。(不能直接定義引用的引用)
例外二、若間接創建一個引用的引用,則這些形參形成了折疊。折疊成一個普通的左值引用類型。只在一種特殊情況下引用會折疊成右值引用:右值引用的右值引用。
3-1)X& &、X& &&和X&& &都折疊成X&
3-2)X&& &&折疊成X&&
注意:引用折疊只能應用于間接創建的引用的引用,如類型別名或模板參數。
如果一個函數參數時指向模板參數類型的右值引用(如,T&&),則可以傳遞給它任意類型的實參,如果一個左值傳遞給這樣的參數,則函數參數被實例化為一個普通的左值引用(T&)
4)編寫接受右值引用參數的模板函數
當傳入一個左值,會改變傳入的實參,這會使得編寫正確的代碼變得困難!
右值引用通常用于兩種情況:
情況1.模板轉發其實參
情況2.模板被重載
- 理解std::move
move的參數是一個指向模板類型參數的右值引用。
針對右值引用的特許規則:雖然不能隱式地將一個左值轉換為右值引用,但可以用static_cast顯式地將一個左值轉換為一個右值引用。
建議:統一使用std::move使得在程序中查找潛在的截斷左值的代碼變得很容易。
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi!"), s2;
s2 = std::move(string("bye!"));
s2 = std::move(s1);
第一個:string&& move(string &&t)
第二個:string&& move(string &t)
- 轉發
某些函數需要將其一個或多個實參連同類型不變地轉發給其他函數。包括是否是const以及實參是左值還是右值。
通過將一個函數參數定義為一個指向模板類型參數的右值引用,可以保持其對應實參的所有類型信息。
在頭文件utility中的forward能保持原始參數的類型。forward返回顯式實參類型的右值引用,即forward<T> 返回的類型是T&&。
注意:std::move和std::forward最好不使用using聲明。
void f(int v1, int &v2); // v2是一個引用
void g(int &&i, int &j);
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {//接受左值引用可以工作,但不能接受右值引用參數的函數
f(t2, t1);
}
flip2(g, i, 42); // 函數參數與其他任何變量一樣,都是左值表達式,因此flip2中對g的調用將傳遞給g的右值引用一個左值。也即將t2傳遞給g的右值引用參數!
//因為這里t2可能被折疊成左值引用,這種情況下傳遞會有問題!
//(而且因為g并不是模板形參,而是一個普通的函數,這里不屬于特殊規則,此時不能將右值引用綁定到左值。)
template <typename Type> intermediary(Type &&arg) {
finalFcn(std::forward<Type>(arg));
}
template<typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) {
f(std::forward<T2>(T2), std::forward<T1>(t1));
}
flip(g, i, 42); // i將以int&類型傳遞給g,42將以int&&類型傳遞給g
-
涉及函數模板的函數匹配規則
注意:正確定義一組重載的函數模板需要對類型間的關系及模板函數允許的有限的實參類型轉換有深刻的理解。
- 重載模板
debug_rep(const T&)本質上可以用于任何類型,包括指針類型。此模板比debug_rep(T*)更通用,后者只能用于指針類型。
當有多個重載模板對一個調用提供同樣好的匹配時,應該選擇最特例化的版本。 - 在定義任何函數之前,記得聲明所有重載的函數版本。這樣就不需要擔心編譯器由于未遇到你希望調用的函數而實例化一個并非你所需的版本。
- 可變參數模板
一個可變參數模板就是一個接受可變數目參數的模板函數或模板類。可變數目的參數被稱為參數包。存在兩種參數包:模板參數包,表示零個或多個模板參數;函數參數包,表示零個或多個函數參數。
模板參數列表:typename...或class...指出接下來的參數表示零個或多個類型的列表;一個類型名后面跟一個省略號表示零個或多個給定類型的非類型參數的列表。
函數參數列表:如果一個參數的類型是一個模板參數包,則此參數也是一個函數參數包。
編譯器從函數實參推斷模板參數類型。
當需要知道包中有多少元素時,可以使用sizeof...運算符。
//Arg是一個模板參數包
//rest是一個函數參數包
template <typename T, typename... Args>
void foo(const T &t, const Arg&... rest);
int i =0;
double d = 3.14;
string s = "how now brown cow";
foo(i, s, 42, d); //包中有三個參數
foo(s, 42, "hi"); //包中有兩個參數
foo(d, s); //包中有一個參數
foo("hi"); //空包
template<typename... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; //類型參數的數目
cout << sizeof...(args) << endl; //函數參數的數目
}
- 編寫可變參數的兩個方法
1)使用initializer_list,所有實參必須具有相同類型
2)當既不知道實參數目,也不知道類型時,可變參數函數很有用 - 編寫可變參數函數模板
當定義可變參數版本的print時,非變參數版本的聲明必須在作用域中。否則,可變參數版本會無限遞歸。
對最后一個調用,兩個函數提供同樣好的匹配。但是,非可變參數模板比可變參數模板更特例化,因此編譯器選擇非可變參數版本。 -
包擴展
擴展一個包就是將它分解成構成的元素,對每個元素應用模式,獲得擴展后的列表。
通過在模式右邊放一個省略號來觸發擴展操作。
print中的函數參數包擴展僅僅將包擴展為其構成元素。error_Msg對每個實參調用debug_rep,這是另一種更復雜的包擴展。
擴展中的模式會獨立地應用于包中的每個元素。
template <typenaem T>
ostream &print(ostream &os, const T &t) {
return os << t;
}
template <typename T, typename ..Args>
ostream &print(ostream &os, const T &t, const Args.. rest) { // 擴展Args
os << t << ", ";
return print(os, rest...); //擴展rest
}
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest) {
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an);
return print(os, debug_rep(rest)...);
}
- 轉發參數包
1)為了保持實參中的類型信息,必須將emplace_back的函數參數定義為模板類型參數的右值引用
2)使用forward來保持實參的原始類型
可變參數函數通常將它們的參數轉發給其他函數。
template <class... Args>
inline
void StrVec::emplace_back(Args&&.. args) {
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...);
}
- 模板特例化
當不能或者不希望使用模板版本時,可以定義類或函數模板的一個特例化版本。
當特例化一個函數模板時,必須為原模板中的每個模板參數都提供實參。template后跟空尖括號對。
一個特例化本質上是一個實例。
注意:模板及其特例化版本應該聲明在同一個頭文件中。所有同名模板的聲明應該放在前面,然后是這些模板的特例化版本。
//可以比較任意兩個類型
template <typename T> int compare(const T&, const T&);
//處理字符串字面常量
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); //調用第一個版本
compare("hi", "mom"); //調用第二個版本
// compare的特例化版本,處理字符數組的指針
template <>
int compare (const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
-
類模板特例化
在定義特例化版本的hash時,唯一復雜的地方是:必須在原模板定義所在的命名空間中特例它。
一個特例化hash類必須定義:
為了讓Sales_data的用戶能使用hash的特例化版本,應該在Sales_data的頭文件中定義該特例化版本。
//定義一個能處理Sales_data的特例化hash版
namespace std{
template <>
struct hash<Sales_data> {
typedef size_t result_type;
typedef Sales_data argument_type;
size_t operator() (const Sales_data& s) const;
};
}
- 類模板的部分特例化本身還是一個模板
部分特例化版本的模板參數列表式原始模板的參數列表的一個子集或者是一個特例化版本。
// 原始的、最通用的版本
template <class T> struct remove_reference {
typedef T type;
};
//部分特例化版本,用于左值引用和右值引用
template <class T> struct remove_reference<T&> {
typedef T type;
};
template <class T> struct remove_reference<T&&> {
typedef T type;
};
int i;
//原始版本
remove_reference<decltype(42)>::type a;
//特例化版本T&
remove_reference<decltype(i)>::type b;
//特例化版本T&&
remove_reference<decltype(std::move(i))>::type c;
- 特例化成員而不是類模板