C++ Primer 筆記-第14章 重載運算與類型轉(zhuǎn)換

14.1 基本概念
重載運算符是具有特殊名字的函數(shù):由關(guān)鍵字?
operator
?和其后要定義的運算符號共同組成。包含返回類型、參數(shù)列表以及函數(shù)體。除了重載的函數(shù)調(diào)用運算符?
operator()
?之外,其他重載運算符不能含有默認(rèn)實參。當(dāng)一個重載的運算符是成員函數(shù)時,
this
?綁定到左側(cè)運算對象。成員運算符函數(shù)的(顯式)參數(shù)數(shù)量比運算對象的數(shù)量少一個。無法改變內(nèi)置類型運算對象的符號含義。
只能重載已有的運算符,而無權(quán)發(fā)明新的運算符號。
重載的運算符其優(yōu)先級和結(jié)合律與對應(yīng)的內(nèi)置運算符保持一致。
直接調(diào)用一個重載的運算符函數(shù):

// 等價的表達(dá)
data1 + data2;
operator+(data1, data2);
data1.operator+(data2);

通常情況下,不應(yīng)該重載逗號
,
、取地址&
、邏輯與&&
?和邏輯或||
?運算符。會無法保留求值順序和/或短路屬性。建議只有當(dāng)操作的含義對于用戶來說清晰明了時才使用運算符。如果用戶對運算符可能有幾種不同的理解,則使用這樣的運算符將產(chǎn)生二義性。

14.2 輸入和輸出運算符
14.2.1 重載輸出運算符?<<
通常,輸出運算符應(yīng)該主要負(fù)責(zé)打印對象的內(nèi)容而非控制格式,輸出運算符不應(yīng)該打印換行符。
如果希望為類自定義IO運算符,則必須將其定義成非成員函數(shù)。IO運算符通常需要讀寫類的非公有數(shù)據(jù)成員,所以IO運算符一般被聲明為友元。
14.2.2 重載輸入運算符?>>
輸入運算符必須處理輸入可能失敗的情況,而輸出運算符不需要。
當(dāng)讀取操作發(fā)生錯誤時,輸入運算符應(yīng)該負(fù)責(zé)從錯誤中恢復(fù)。

14.3 算術(shù)與關(guān)系運算符
如果類同時定義了算術(shù)運算符和相關(guān)的復(fù)合賦值運算符,則通常情況下應(yīng)該使用復(fù)合賦值來實現(xiàn)算術(shù)運算符。
14.3.1 相等運算符?==
如果某個類在邏輯上有相等性的含義,則該類應(yīng)該定義?
operator==
,這樣做可以使得用戶更容易使用標(biāo)準(zhǔn)庫算法來處理這個類。
14.3.2 關(guān)系運算符?<
?>
?<=
?>=
如果存在唯一一種邏輯可靠的 < 定義,則應(yīng)該考慮為這個類定義 < 運算符。如果類同時還包含 ==,則當(dāng)且僅當(dāng) < 的定義和 == 產(chǎn)生的結(jié)果一致時才定義 < 運算符。

14.4 賦值運算符?=
可以重載賦值運算符。不論形參的類型是什么,賦值運算符都必須定義為成員函數(shù)。
賦值運算符必須定義成類的成員,復(fù)合賦值運算符通常情況下也應(yīng)該這樣做。這兩類運算符都應(yīng)該返回左側(cè)運算對象的引用。

14.5 下標(biāo)運算符?[]
下標(biāo)運算符必須是成員函數(shù)。
如果一個類包含下標(biāo)運算符,則它通常會定義兩個版本:一個返回普通引用,另一個是類的常量成員并且返回常量引用。

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;
}


14.6 遞增和遞減運算符?++
?--
定義遞增和遞減運算符的類應(yīng)該同時定義前置版本和后置版本。這些運算符通常應(yīng)該被定義成類的成員。
為了與內(nèi)置版本保持一致,前置運算符應(yīng)該返回遞增或遞減后對象的引用。
后置版本接受一個額外的(不被使用)int 類型的形參。讓編譯器區(qū)分前后置版本。
為了與內(nèi)置版本保持一致,后置運算符應(yīng)該返回對象的原值(遞增或遞減之前的值,需額外拷貝一份原值的副本),返回的形式是一個值而非引用。

class StrBlobPtr?{
public:
? ?// 前置版本
? ?StrBlobPtr& operator++();
? ?StrBlobPtr& operator--();
? ?// 后置版本
? ?// 不會用到int形參,無須為其命名
? ?StrBlobPtr operator++(int);
? ?StrBlobPtr operator--(int);
}
StrBlobPtr p;
// 前置版本調(diào)用
++p;
p.operator++();
// 后置版本調(diào)用
p++;
p.operator++(0);


14.7 成員訪問運算符?*
?->
箭頭運算符必須是類的成員。解引用運算符通常也是類的成員,盡管并非必須如此。
箭頭運算符永遠(yuǎn)不能丟掉訪問成員這個最基本的含義,當(dāng)重載箭頭時,可以改變的是箭頭從哪個對象當(dāng)中獲取成員,而箭頭獲取成員這一事實則永遠(yuǎn)不變。
重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象,
point->mem
的執(zhí)行過程如下所示:
(1) 如果?point
?是指針,則應(yīng)用內(nèi)置的箭頭運算符,表達(dá)式等價于?(*point).mem
。首先解引用該指針,然后從所得的對象中獲取指定的成員。如果?point
?所指的類型沒有?mem
?的成員,程序會發(fā)生錯誤。
(2) 如果?point
?是定義了?operator->
?的類的一個對象,則使用?point.operator->()
?的結(jié)果來獲取?mem
。其中,如果該結(jié)果是一個指針,則執(zhí)行第1步;如果該結(jié)果本身含有重載的?operator->()
,則重復(fù)調(diào)用當(dāng)前步驟。最終,當(dāng)這一過程結(jié)束時,程序或者返回了所需的內(nèi)容,或者返回一些表示程序錯誤的信息。

14.8 函數(shù)調(diào)用運算符?()
如果類重載了函數(shù)調(diào)用運算符,則我們可以像使用函數(shù)一樣使用該類的對象。
函數(shù)調(diào)用運算符必須是成員函數(shù)。一個類可以定義多個不同版本的調(diào)用運算符,相互之間應(yīng)該在參數(shù)數(shù)量或類型上有所區(qū)別。
如果類定義了調(diào)用運算符,則該類的對象稱做函數(shù)對象(function object)。

PrintString?{
public:
? ?PrintString(ostream& o = cout) : os(o) {}
? ?void operator() (const string& s) const { os << s; }
private:
? ?ostream& os;
}
// 像調(diào)用函數(shù)一樣,調(diào)用函數(shù)對象
PrintString printer;? ? ? ??// 使用默認(rèn)值,打印到cout
printer(s);? ? ? ? ? ? ? ? ? ? ?// 在cout中打印s
PrintString errors(cerr);// 打印到cerr
errors(s);? ? ? ? ? ? ? ? ? ? ? // 在cerr中打印s
// 函數(shù)對象常常作為泛型算法的實參
// 第三個參數(shù)是類型PrintString的一個臨時對象
vector<string> vs;
for_each(vs.begin(), vs.end(), PrintString(cerr));

14.8.1 lambda是函數(shù)對象
編寫一個lambda表達(dá)式后,編譯器會將該表達(dá)式翻譯成一個未命名類的未命名對象。在lambda表達(dá)式產(chǎn)生的類中含有一個重載的函數(shù)調(diào)用運算符。
當(dāng)一個lambda表達(dá)式通過引用捕獲變量時,將由程序負(fù)責(zé)確保lambda執(zhí)行時引用所引的對象確實存在。因此編譯器可以直接使用該引用而無須在lambda產(chǎn)生的類中將其存儲為數(shù)據(jù)成員。
相反,通過值捕獲的變量被拷貝到lambda中。因此,這種lambda產(chǎn)生的類必須為每個值捕獲的變量建立對應(yīng)的數(shù)據(jù)成員,同時創(chuàng)建構(gòu)造函數(shù),令其使用捕獲的變量的值來初始化數(shù)據(jù)成員。
lambda表達(dá)式產(chǎn)生的類不含默認(rèn)構(gòu)造函數(shù)、賦值運算符以及默認(rèn)析構(gòu)函數(shù)。它是否含有默認(rèn)拷貝函數(shù)/移動構(gòu)造函數(shù)則通常要視捕獲的數(shù)據(jù)成員類型而定。

auto wc = find_if(word.begin(), word.end(),
? ? ? ? ? ?[sz](const string& a)
? ? ? ? ? ? ? ?{ return a.size() >= sz; });
// 該lambda表達(dá)式產(chǎn)生的類將形如:
class SizeComp?{
public: ? ?SizeComp(size_t n) : sz(n) {} ? // 該形參對應(yīng)捕獲的變量
? ?// 該調(diào)用運算符的返回值類型、形參和函數(shù)體都與lambda一致
? ?bool operator() (const string& s) const
? ? ? ?{ return s.size() >= sz; }
private:
? ?size_t sz; ?// 該數(shù)據(jù)成員對應(yīng)通過值捕獲的變量
}
// 等價的調(diào)用:
auto wc = find_if(word.begin(), word.end(), SizeComp(sz));

14.8.2 標(biāo)準(zhǔn)庫定義的函數(shù)對象
標(biāo)準(zhǔn)庫定義了一組表示算術(shù)運算符、關(guān)系運算符和邏輯運算符的類,每個類分別定義了一個執(zhí)行命名操作 的調(diào)用運算符。(
plus<Type>
、equal_to<Type>
、logical_and<Type>
等)這些類都被定義成模板的形式,我們可以為其指定具體的應(yīng)用類型,這里的類型即調(diào)用運算符的形參類型。
表示運算符的函數(shù)對象類常用于替換算法中的默認(rèn)運算符。

sort(svec.begin(), svec.end(), greater<string>());

14.8.3 可調(diào)用對象與function
c++中有五種可調(diào)用對象:函數(shù)、函數(shù)指針、lambda表達(dá)式、bind創(chuàng)建的對象以及重載了函數(shù)調(diào)用運算符的類。
和其他對象一樣,可調(diào)用的對象也有類型。例如,每個lambda有它自己唯一的(未命名)類類型;函數(shù)即函數(shù)指針的類型則由其返回值類型和實參類型決定,等等。
兩個不同類型的可調(diào)用對象卻可能共享同一種調(diào)用形式(call signature)。調(diào)用形式指明了調(diào)用返回的類型以及傳遞給調(diào)用的實參類型。一種調(diào)用形式對應(yīng)一個函數(shù)類型,例如:
int (int, int)
?是一個函數(shù)類型,它接受兩個int、返回一個int。

// 下列不同類型的可調(diào)用對象,但是共享了同一種調(diào)用形式int(int, int):
// 普通函數(shù)
int add(int i, int j) { return i + j;}
// lambda,其產(chǎn)生一個未命名的函數(shù)對象
auto mod = [](int i, int j) { return i % j; };
// 函數(shù)對象
struct divide {
? ?int operator() (int i, int j) { return i / j; }
}
// 構(gòu)建從運算符到函數(shù)指針的映射關(guān)系,其中函數(shù)接受兩個int,返回一個int
map<string, int(*)(int, int)> binops;
binops.insert({ "+", add }); // 正確:add是一個指向正確類型函數(shù)的指針
binops.insert({ "%", mod }); // 錯誤:mod不是一個函數(shù)指針

可以使用標(biāo)準(zhǔn)庫的?
function
?類型解決函數(shù)指針不匹配的問題。function是一個模板,當(dāng)創(chuàng)建一個具體的function類型時我們必須提供額外的信息,在尖括號內(nèi)指定類型:
function<int(int, int)>
。

// 將之前的binops類型該為使用function<>
map<string, function<int(int, int)>> binops;
// 可以把所有可調(diào)用對象都添加到這個map中:
binops = {
? ?{"+", add}, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 函數(shù)指針
? ?{"-", std::minus<int>()}, ? ? ? ? ? ? ? ?// 標(biāo)準(zhǔn)庫函數(shù)對象
? ?{"/", divide()}, ? ? ? ? ? ? ? ? ? ? ? ? // 用戶定義的函數(shù)對象
? ?{"*", [](int i, int j) {return i * j;}}, // 未命名的lambda
? ?{"%", mod}, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 命名了的lambda對象
}
// function類型重載了調(diào)用運算符,該運算符接受它自己的實參然后將其傳遞給存好的調(diào)用對象:
binops["+"](10, 5); ?// 調(diào)用add(10, 5)
binops["-"](10, 5); ?// 調(diào)用minus<int>對象的調(diào)用運算符
binops["/"](10, 5); ?// 調(diào)用divide對象的調(diào)用運算符
binops["*"](10, 5); ?// 調(diào)用lambda函數(shù)對象
binops["%"](10, 5); ?// 調(diào)用lambda函數(shù)對象

不能直接將重載函數(shù)的名字存入?
function
?類型對象中:

int add(int i, int j) { return i + j;}
Sales_data add(const Sales_data&, const Sales_data&);
binops.insert( {"+", add} );// 錯誤:哪個add?
// 解決二義性問題的方法有兩條
// 方法一:存儲函數(shù)指針為非函數(shù)名字
int (*fp)(int, int) = add;? ??// 指針?biāo)傅腶dd是接受兩個int的版本
binops.insert( {"+", fp} ); // 正確:fp指向一個正確的add版本
// 方法二:使用lambda來消除二義性
binops.insert( {"+", [](int i, int j) { return add(i, j); }} );


14.9 重載、類型轉(zhuǎn)換與運算符
轉(zhuǎn)換構(gòu)造函數(shù)和類型轉(zhuǎn)換運算符共同定義了類類型轉(zhuǎn)換(class-type conversions),這樣的轉(zhuǎn)換有時也被稱作用戶定義的類型轉(zhuǎn)換(user-defined conversion)。
14.9.1 類型轉(zhuǎn)換運算符
類型轉(zhuǎn)換運算符(conversion operator)是類的一種特殊成員函數(shù),它負(fù)責(zé)將一個類類型的值轉(zhuǎn)換成其他類型。
operator type() const;
類型轉(zhuǎn)換函數(shù)必須是類的成員函數(shù),它不能聲明返回類型,形參列表必須為空。類型轉(zhuǎn)換函數(shù)通常應(yīng)該是?
const
。類型轉(zhuǎn)換函數(shù)不允許轉(zhuǎn)換成數(shù)組或函數(shù)類型,但是允許轉(zhuǎn)化成指針(包括數(shù)組指針及函數(shù)指針)或者引用類型。

class SmallInt {
public:
? ?// 構(gòu)造函數(shù)將算術(shù)類型的值轉(zhuǎn)換成SmallInt對象
? ?SmallInt(int i = 0) : val(i) {}
? ?// 類型轉(zhuǎn)換運算符將SmallInt對象轉(zhuǎn)換成int
? ?operator int() const { return val; }
private:
? ?std::size_t val;
}

C++11新標(biāo)準(zhǔn)引入了顯示的類型轉(zhuǎn)換運算符(explicit conversion operator):

class SmallInt {
public:
? ?explicit operator int() const { return val; }
? ?// ... ...
}
SmallInt si = 3; // 正確:構(gòu)造函數(shù)不是顯式的
si + 3; ??????????????// 錯誤:此處需要隱式的類型轉(zhuǎn)換,但類的運算符是顯式的
static_cast<int>(si) + 3; // 正確:顯式地請求類型轉(zhuǎn)換

存在一個例外,如果表達(dá)式被用作條件,則編譯器會將顯式的類型轉(zhuǎn)換自動轉(zhuǎn)成隱式的執(zhí)行。
向?
bool
?的類型轉(zhuǎn)換通常用在條件部分,因此?operator bool()
?一般定義成?explicit
?的。
14.9.2 避免有二義性的類型轉(zhuǎn)換
不要令兩個類執(zhí)行相同的類型轉(zhuǎn)換:如果?
A
?類有一個接受?B
?類對象的構(gòu)造函數(shù),則不要在?B
?類中再定義轉(zhuǎn)換目標(biāo)是?A
?類的類型轉(zhuǎn)換運算符。

struct B;
struct A {
? ?A() = defult;
? ?A(const B&); ? ?// 把一個B轉(zhuǎn)換成A
};
struct B {
? ?operator A() const; // 也是把一個B轉(zhuǎn)換成A
};
A f(const A&);
B b;
// 二義性錯誤:含義是f(B::operator A())
// 還是f(A::A(const B&))?
A a = f(b);
// 正確:
A a1 = f(b.operator A());
A a2 = f(A(b));

避免轉(zhuǎn)換目標(biāo)是內(nèi)置算術(shù)類型的類型轉(zhuǎn)換。特別是當(dāng)你已經(jīng)定義了一個轉(zhuǎn)換成算術(shù)類型的類型轉(zhuǎn)換時,接下來:
(1) 不要再定義接受算術(shù)運算符的重載運算符。如果用戶需要使用這樣的運算符,則類型轉(zhuǎn)換操作將轉(zhuǎn)換你的類型的對象,然后使用內(nèi)置的運算符。
(2) 不要定義轉(zhuǎn)換到多種算術(shù)類型的類型轉(zhuǎn)換。讓標(biāo)準(zhǔn)類型轉(zhuǎn)換完成向其他算術(shù)類型轉(zhuǎn)換的工作。

struct A {
? ?// 兩種算術(shù)類型的類型轉(zhuǎn)化
? ?A(int);
? ?A(double);?
? ?operator int() const;
? ?operator double() const;
}
void f2(long double);
A a;
// 二義性錯誤:含義是f(A::operator int())
// 還是f(A::operator double())?
f2(a);
long lg;
// 二義性錯誤:含義是A::A(int)還是A::A(double)?
A a2(lg);

當(dāng)調(diào)用重載的函數(shù)時,從多個類型轉(zhuǎn)換種進(jìn)行選擇將變得更加復(fù)雜。如果兩個或多個類型轉(zhuǎn)換都提供了同一種可行匹配,則這些類型轉(zhuǎn)化一樣好。
如果在調(diào)用重載函數(shù)時我們需要使用構(gòu)造函數(shù)或者強(qiáng)制類型轉(zhuǎn)換來改變實參的類型,則這通常意味著程序的設(shè)計存在不足。

struct C {
? ?C(int);
};
struct D {
? ?D(int);
};
void manip(const C&);
void manip(const D&);
// 二義性錯誤:含義是manip(C(10))還是manip(D(10))
manip(10);

在調(diào)用重載函數(shù)時,如果需要額外的標(biāo)準(zhǔn)類型轉(zhuǎn)換,則該轉(zhuǎn)換的級別只有當(dāng)所有可行函數(shù)都請求同一個用戶定義的類型轉(zhuǎn)換時才有用。如果所需的用戶定義的類型轉(zhuǎn)換不止一個,則該調(diào)用具有二義性:

struct E {
? ?E(double);
}
void manip2(const C&);
void manip2(const E&);
// 二義性錯誤:兩個不同的用戶定義的類型轉(zhuǎn)換都能用在此處。
// 含義是manip2(C(10)) 還是manip2(E(double(10)))
manip2(10);

14.9.3 函數(shù)匹配與重載運算符
表達(dá)式中運算符的候選函數(shù)集既包括成員函數(shù),也包括非成員函數(shù)。當(dāng)我們在表達(dá)式中使用重載的運算符時,無法判斷正在使用的是成員函數(shù)還是非成員函數(shù)。
如果我們對同一個類既提供了轉(zhuǎn)換目標(biāo)是算術(shù)類型的類型轉(zhuǎn)換,也提供了重載的運算符,則將會遇到重載運算符與內(nèi)置運算符的二義性問題。

class SmallInt {
? ?friend
? ?SmallInt operator+(const SmallInt&, const SmallInt&);
public:
? ?SmallInt(int = 0); ? ? ? ? ? ? ? ? ? // 轉(zhuǎn)換源為int的類型轉(zhuǎn)換
? ?operator int() const { return val; } // 轉(zhuǎn)換目標(biāo)為int的類型轉(zhuǎn)換
private:
? ?std::size_t val;
}
SmallInt s;
// 二義性錯誤:
// 可以把0轉(zhuǎn)換成SmallInt,然后使用SmallInt的+,再轉(zhuǎn)成int
// 也可以把s轉(zhuǎn)成int,然后對兩個int執(zhí)行內(nèi)置的加法運算。
int i = s + 0;


