重載運算符是具有特殊名字的函數: 它們的名字由關鍵字operator和其后要定義的運算符號共同組成.
重載運算符函數的參數數量與該運算符作用的運算對象數量一樣多.
對於二元運算符來說, 左側運算對象傳遞給第一個參數, 而右側運算對象傳遞給第二個參數.
除了重載的函數調用運算符operator()除外, 其他重載運算符不能含有默認實參.
如果一個運算符函數是成員函數, 則它的第一個(左側)運算對象綁定到隱式的this指針上, 因此成員運算符函數的顯式參數數量比運算符的運算對象總數少一個.
對於一個運算符函數來說, 它或者是類的成員, 或者至少有一個類類型的參數. 如:
int operator+(int, int); // 錯誤, 不能為int重定義內置的運算符
這意味着當運算符作用於內置類型時, 我們無法改變運算符的含義.
只能重載已有的運算符, 而無權發明新的運算符號, 對於重載的運算符來說, 其優先級和結合律與對應的內置運算符保持一致.
不能被重載的運算符: "::" ".*" "." "? :"
一般情況下不應該被重載的運算符: 逗號, 取地址, 邏輯與和邏輯或運算符.
非成員運算符函數的調用:
data1 + data2; // 普通的表達式 operator+(data1, data2); // 等價的函數調用
成員運算符函數的調用:
data1 += data2; // 基於“調用”的形式 data1.operator+=(data2); // 對成員運算符函數的等價調用
選擇作為成員函數或者非成員函數
有些運算符必須作為成員函數, 另一些情況下, 運算符作為普通函數比作為成員好.
賦值(=), 下標([ ]), 調用(())和成員訪問箭頭(->)運算符必須是成員函數.
復合賦值符運算符一般來說應該是成員, 但這並非必須, 這一點與賦值運算符略有不同.
改變對象狀態的運算符或者與給定類型密切相關的運算符, 如遞增, 遞減和解引用運算符, 通常應該是成員函數.
具有對稱性的運算符可能轉換任意一端的運算對象, 如算術, 相等性, 關系和位運算符等, 它們通常應該是普通的非成員函數.
必須為成員函數的運算符函數有4個: =, [], (), ->.
建議為成員函數的運算符函數: 復合賦值運算符(+=, -=, *=, /=), 自增, 自減, 解引用.
必須為非成員: 流操作運算符(<<, >>).
建議非成員: 算術, 關系, 位操作.
如果我們想提供含有類對象的混合類型表達式, 則運算符必須定義成非成員函數. 因為我們把運算符定義成成員函數時, 它的左側運算對象必須是運算符所屬類的一個對象. 如:
string = "world"; string t = s + "!"; // 正確, 能夠把一個const char * 加到一個string對象中. string u = "hi" + s; // 如果+是string的成員, 則產生錯誤.
如果operator+是string類的成員, 則上面的第一個加法等價於s.operator("!"). 同樣的, "hi"+s等價於"hs".operator(s). 顯然, "hi"的類型是const char *, 這是一種內置類型, 根本沒有成員函數.
因為string將+定義成了普通的非成員函數, 所以"hi"+s等價於operator("hi", s), 每個實參都能被轉換成形參類型. 唯一的要求是至少有一個運算對象是類類型, 且兩個運算對象都能轉換成string.
賦值運算符應該返回它左側運算對象的一個引用.
1 輸入輸出運算符
1.1 重載輸出運算符
通常情況下, 輸出運算符的第一個形參是非常量ostream對象的引用. 之所以ostream是非常量是因為向流寫入內容會改變其狀態, 形參是引用是因為我們無法拷貝ostream.
第二個形參一般來說是一個常量引用, 該常量是我們想要打印的類類型. 是引用的原因是因為我們希望避免賦值實參, 為常量是因為打印通常不會改變對象的內容.
為了與其他輸出運算符保持一致, operator<<一般要返回它的ostream形參.
ostream &operator<<(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; }
通常輸出運算符只負責打印內容而非控制格式, 輸出運算符不應該打印換行符.
輸入輸出運算符必須是普通的非成員函數, 而不能是類的成員函數. 否則它的左側運算對象將是我們的類的一個對象.
如果是類的成員的話, 則第一個參數就必須是類的對象, 但是輸入輸出運算符的左側運算對象(第一個參數)是ostream對象.
IO運算符通常需要讀寫類的非公有數據成員, 所以IO運算符一般被聲明為友元.
1.2 重載輸入運算符
通常情況下, 輸入運算符的第一個形參是運算符將要讀取的流的引用, 第二個形參是將要讀入到的(非常量的)對象的引用. 返回給定流的引用.
輸入運算符必須處理輸入可能失敗的情況, 而輸出運算符則不需要.
istream &operator>>(istream &is, Sales_data &item) { double price; is >> item.bookNo >> item.units_sold >> price; if(is) // 檢查輸入是否成功 item.revenue = item.units_sold * price; else // 輸入失敗, 對象被賦予默認的狀態. 這里並不關心哪一部分讀取失敗. item = Sales_data(); return is; }
輸入運算符可能發生如下錯誤:
流含有錯誤類型的數據時讀取操作可能失敗.
讀取操作到達文件結尾或者輸入流遇到其他錯誤時也會失敗.
2 算術和關系運算符
通常情況下把算術和關系運算符定義成非成員函數以允許對左側或右側的運算對象進行轉化, 因為這些運算符一般不需要改變運算對象的狀態, 所以形參都是常量引用.
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs) { Sales_data sum = lhs; sum += rhs; // 用復合賦值運算符來實現算術運算符 return sum; }
如果類同時定義了復合賦值運算符, 則通常情況下應該使用復合賦值運算符來實現算術運算符.
2.1 相等運算符
bool operator==(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue; } bool operator!=(const Sales_data &lhs, const Sales_data &rhs) { return !(lhs == rhs); // 具體工作交由==來完成. }
如果類定義了==操作, 則通常也應該定義!=操作.
相等運算符和不相等運算符中的一個應該把工作委托給另外一個.
2.2 關系運算符
定義了相等運算符的類常常(但不總是)包含關系運算符.
通常情況下, 關系運算符應該:
定義順序關系, 令其與關聯容器中對關鍵字的要求一致.
如果類同時也含有==運算符的話, 則定義一種關系令其與==保持一致, 特別是, 如果兩個對象是!=的, 那么一個對象應該<另外一個.
Sales_data類的相等比較是逐個比較成員, 但是由於有多個成員, <操作的邏輯不明確. Sales_data類不存在一種邏輯可靠的<定義, <和==產生的結果不一致, 這個類不定義<運算也許會更好.
如果存在唯一一種邏輯可靠的<定義, 則應該考慮為這個類定義<運算符. 如果類同時還包含==, 則當且僅當<的定義和==產生的結果一致時才定義<運算符.
3 賦值運算符
除拷貝賦值運算符外, 類還可以定義其他賦值運算符以使用其他類型作為右側運算對象.
賦值運算符返回左側運算對象的引用.
賦值運算符必須定義為成員函數.
與拷貝賦值和移動賦值運算符一樣, 其他重載的賦值運算符也必須先釋放當前的內存空間, 在創建一片新空間. 但是無需檢查自賦值的情況, 因為其形參不是自身類型, 這確保形參與自身不是同一個對象.
StrVec &StrVec::operator=(initalizer_list<string> il) { auto data = alloc_n_copy(il.begin(), il.end()); // 無需檢查自賦值情況. free(); elements = data.first; first_free = cap = data.second; return *this; }
4 復合賦值運算符
復合賦值運算符不非得是類的成員, 不過我們還是傾向於把包括復合賦值在內的所有賦值運算都定義在類的內部. 這兩類運算符都應該返回左側運算對象的引用.
Sales_data &Sales_data::operator+=(const Sales_data &rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
5 下標運算符
下標運算符必須是成員函數.
為了與下標的原始定義相兼容, 下標運算符以所訪問元素的引用作為返回值, 這樣做的好處是下標可以出現在賦值運算符的任意一端.
最好同時定義下標運算的常量版本和非常量版本, 當作用於常量對象時, 下標運算符返回常量引用以確保我們不會給返回的對象賦值.
class StrVec{ public: std::string &operator[](std::siez_t n) // 非常量版本 { return elements[n]; } const std::string &operator[](std::size_t n) const // 常量版本 { return elements[n]; } private: std::string *elements; // 指向數組首元素的指針 };
6 遞增和遞減運算符
遞增遞減改變所操作對象的狀態, 所以建議將其設定為成員函數.
定義遞增和遞減運算符的類應該同時定義前置和后置兩個版本.
為了與內置版本一致, 前置運算符應該返回遞增或遞減后的對象的引用. 后置運算符應該返回對象的原值(遞增前或遞減前的值), 返回形式是一個值而非引用.
前置版本:
class StrBlobPtr{ public: StrBlobPtr &operator++(); // 前置版本, 返回類型是StrBlobPtr的引用類型 StrBlobPtr &operator--(); }; StrBlobPtr &StrBlobPtr::operator++() { check(curr, "incremet past end of StrBlobPtr"); ++curr; return *this; } StrBlobPtr &StrBlobPtr::operator--() { --curr; check(curr, "incremet past end of StrBlobPtr"); return *this; }
區分前置和后置版本
前置版本和后置版本使用的是同一個符號, 意味着其重載版本所用的名字將是相同的, 並且運算對象的數量和類型也相同.
為了將二者區分開來, 后置版本接受一個額外的(但並不使用)int類型的形參, 當我們使用后置版本運算符時, 編譯器為這個形參提供一個值為0的實參. 這個形參的唯一作用就是區分前置版本和后置版本的函數, 而不是真正的要在實現后置版本時參與運算.
class StrBlobPtr{ public: StrBlobPtr operator++(int);// 后置版本, 返回類型為值類型, 而非引用, 多了一個int形參 StrBlobPtr operator--(int); }; StrBlobPtr StrBlobPtr::operator++(int) { // 此處無需檢查有效性, 調用前置遞增運算時才需要檢查. StrBlobPtr ret = *this; // 后置運算符調用各自的前置版本來完成實際工作 ++*this; // 向前移動一個元素, 調用已定義的前置遞增運算符, 有效性的檢查在前置遞增中完成. return ret; } StrBlobPtr StrBlobPtr::operator--(int) { // 此處無需檢查有效性, 調用前置遞減運算時才需要檢查. StrBlobPtr ret = *this; --*this; // 后移動一個元素, 調用已定義的前置遞減運算符, 有效性的檢查在前置遞減中完成. return ret; } // 顯式地調用后置運算符 StrBlobPtr p(a1); p.operator++(0); // 調用后置版本的operator++,p++ p.operator++(); // 調用前置版本的operator++,++p
7 成員訪問運算符
class StrBlobPtr{ public: std::string& operator*() const // 返回引用 { auto p = check(curr, "dereference past end"); return (*p)[curr]; } std::string* operator->() const // 返回指針 { return &this->operator*(); // 將實際工作委托給解引用運算符 } };
箭頭運算符必須是類成員, 解引用運算符通常也是類的成員, 盡管並非必須如此.
重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象.
對於形如point->mem的表達式來說, point必須是指向類對象的指針或者是一個重載了operator->類的對象, 其執行過程如下:
如果point是指針, 則我們應用內置的箭頭運算符, 表達式等價於(*point).mem, 首先解引用該指針, 然后從所得的對象中獲取指定的成員.
如果point是定義了operator->的類的一個對象, 則我們使用point.operator->()的結果來獲取mem. 其中, 如果該結果是一個指針, 則執行第1步, 如果該結果本身含有重載的operator->(), 則重復調用當前步驟.
重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象.
8 函數調用運算符
如果類重載了函數調用運算符, 則我們可以像使用函數一樣使用該類的對象. 因為這樣的類同時也能存儲狀態, 所以與普通函數相比他們更加靈活.
struct absInt { int operator()(int val) const { return val < 0 ? -val : val; } }; int i = 42; absInt absObj; // 含有函數調用運算符的對象 int ui = absObj(i); // 將i傳遞給absObj.operator()
函數調用運算符必須是成員函數, 一個類可以定義多個不同版本的調用運算符, 相互之間應該在參數或類型上有所區別.
如果類定義了調用運算符, 則該類的對象稱作函數對象. 因為可以調用這種對象, 所以我們說這些對象的行為像函數一樣.
含有狀態的函數對象類
和其他類一樣, 函數對象類除了operator()之外也可以包含其他成員. 函數對象類通常含有一些數據成員, 這些成員被用於定制調用運算符中的操作. 如
// 該類用於定制打印字符串的操作, 每一個字符串之后打印一個sep字符. class PrintString{ public: PrintString(std::ostream &o = std::cout, char c = ' ') : os(o), sep(c) { }; void operator()(const std::string &s) const { os << s << sep; }; private: std::ostream &os; char sep; }; std::string s{"Hello"}; PrintString ps; // 使用默認值, 打印到cout ps(s); PrintString ps2(std::cout, '\n'); ps2(s); // 函數對象常常作為泛型算法的實參 std::for_each(svec.begin(), svec.end(), PrintString(std::cerr, '\n'));
8.1 lambda是函數對象
編譯器將lambda表達式翻譯成一個未命名類的未命名對象. 所以必須要用auto來獲取lambda的類型. lambda有他自己唯一的類類型, 只不過是未命名的.
在lambda表達式產生的類中含有一個重載的函數調用運算符. 如
stable_sort(svec.begin(), svec.end(), [](const string &a, const string &b){ return a.size() < b.size(); }); // 與lambda表達式的等價操作 class ShorterString { public: bool operator()(const string &a, const string &b) const { return a.size() < b.size(); } }; stable_sort(svec.begin(), svec.end(), ShorterString());
表示lambda及相應捕獲行為的類
當一個lambda表達式通過引用捕獲變量時, 將由程序確保lambda執行時引用所引的對象確實存在. 因此, 編譯器可以直接使用該引用而無需再lambda產生的類中將其存儲為數據成員.
通過值捕獲變量時, 由於變量是被拷貝到lambda中的, 因此這種lambda產生的類必須為每個值捕獲的變量建立對應的數據成員, 同時創建構造函數, 令其使用捕獲的變量的值來初始化數據成員.
// 返回第一個指向滿足條件元素的迭代器, 該元素滿足 size > sz; auto wc = find_if(svec.begin(), svec.end(), [sz](const string &a){ return a.size() > sz; }); // 該lambda表達式產生的類形如 class SizeComp { public: SizeComp(size_t size): sz(size) { } // 該形參對用捕獲的變量 // 調用該運算符的返回類型, 形參和函數體都與lambda保持一致 bool operator()(const string &a) { return a.size() > sz; } private: size_t sz; // 該數據成員對應通過值捕獲的變量 }; auto wc = find_if(svec.begin(), svec.end(), SizeComp(sz));
lambda表達式產生的類不含有默認構造函數, 賦值運算符及默認析構函數. 是否含有默認拷貝/移動函數則要視捕獲的數據成員類型而定.
8.2 標准庫定義的函數對象
標准庫定義了一組表示算術運算符, 關系運算符和邏輯運算符的類, 每個類分別定義了一個執行命名操作的調用運算符. 這些類型都定義在functional頭文件中.
算術 |
plus<Type> |
minus<Type> |
multiplies<Type> |
divides<Type> |
modulus<Type> |
negate<Type> |
plus<int> intAdd; // 執行int加法操作 negate<int> intNegate; // 執行int取反操作 // 使用 intAdd::operator(int, int) int sum = intAdd(10, 24); // 10 + 24 // 使用 intNegate::operator(int) sum = intNegate(intAdd(10, 20)); // -(10 + 20) // 在算法中使用標准庫函數對象 vector<int> ivec{2, 3, 1, 7, 6}; sort(ivec.begin(), ivec.end(), greater<int>()); // 按 > 符號排序 for_each(ivec.begin(), ivec.end(), [](int i){ cout << i << " "; });
8.3 可調用對象與function
C++語言中有幾種可調用對象: 函數, 函數指針, lambda表達式, bind創建的對象以及重載了函數調用運算符的類.
然而, 兩個不同類型的可調用對象卻可能共享同一種調用形式. 調用形式指明了調用返回類型以及傳遞個調用的實參類型. 一種調用形式對應一個函數類型.
不同類型可能具有相同的調用形式.
// 普通函數 int add(int i, int j) { return i + j; } // lambda, 其產生一個未命名的函數對象類 auto mod = [](int i, int j) { return i % j; }; // 函數對象類 struct divide{ public: int operator()(int denominator, int divisor) { return denominator / divisor; } };
函數表: 用於存儲指針指向可調用對象的“指針”. 在C++語言中, 函數表很容易通過map來實現.
// 構建從運算符到函數指針的映射關系, 其中函數接收兩個int, 返回一個int map<string, int(*)(int, int)> binops; // 正確: add是指向正確類型的函數指針 binops.insert({"+", add}); // 錯誤: mod不是函數指針, 同理也不能將divide存入binops中 binops.insert({"%", mod});
但是我們不能將mod或者divide存入binops. 問題在於mod是個lambda表達式, 而每個lambda有它自己的類類型, 該類型與binops中的值得類型不匹配.
可以使用function的標准庫類型來解決上述問題, function定義在functional頭文件中.
function的操作:
function<T> f;
function<T> f(nullptr);
function<T> f(obj);
f
f(args)
定義為function<T>的成員的類型
result_type
argument_type
first_argument_type
second_argument_type
function是一個模板, 創建一個具體的function類型時我們必須提供額外的信息, 及對象的調用形式. 如
function<int(int, int)> f1 = add; // 函數指針 function<int(int, int)> f2 = mod; function<int(int, int)> f3 = divide(); // 函數對象類對象 function<int(int, int)> f4 = [](int i, int j){ return i * j; }; cout << f1(10, 5) << endl; // 15 cout << f3(10, 5) << endl; // 2 map<string, function<int(int, int)>> binops = { {"+", add}, // 函數指針 {"-", std::minus<int>()}, // 標准庫函數對象 {"/", divide()}, // 用戶定義的函數對象 {"*", [](int i, int j) { return i * j; }},// 未命名的lambda {"%", mod} }; // 命名lambda cout << binops["+"](10, 5) << endl; // 15 cout << binops["-"](10, 5) << endl; // 5 cout << binops["/"](10, 5) << endl; // 2
8.4 重載函數與function
不能直接將重載函數的名字存入function類型的對象中:
int add(int i, int j) { return i + j; } float add(float i, float j) { return i + j; } // 函數重載 map<string, function<int(int, int)>> binops; binops.insert({"+", add}); // 錯誤, 那個add ? 有二義性 // 解決上述二義性問題的一條途徑是存儲函數指針而非函數名字 int (*fp)(int, int) = add; binops.insert({"+", fp}); // 正確 // 或者用lambda表達式 binops.insert({"+", [](int i, int j) { return add(i, j); }});
9 重載, 類型轉換與運算符
一個實參調用的非顯式構造函數定義了種隱式的類型轉換, 這種構造函數將實參類型的對象轉換成類類型. 也可以通過類型轉換運算符來定義對於類類型的轉換.
轉換構造函數和類型轉換運算符共同定義了類類型轉換, 這樣的轉換有時也被稱為用戶定義的類型轉換.
轉換構造函數: 其他類型----------->類類型
類型轉換運算符: 類類型----------->其它類型
9.1 類型轉換運算符
類型轉換運算符是一種特殊的成員函數, 它負責將一個類類型的值轉換成其他類型. 類型轉換函數的一般形式如下:
operator type() const;
其中type表示某種類型. 類型轉換運算符可以面向任意類型(除void之外)進行定義, 只要該類型能夠作為函數的返回類型. 因此不允許轉換成函數類型或數組, 但允許轉換成指針或者引用類型.
一個類型轉換函數必須是類的成員函數, 它不能聲明返回類型, 形參列表也必須為空, 類型轉換函數通常應該是const的.
class SmallInt{ public: // 非顯式轉換構造函數, 定義其他類型向類類型轉換 SmallInt(int i = 0) : val(i) { if(i < 0 || i > 255) throw std::out_of_range("Bad SmallInt value"); } // 類型轉換運算符, 類類型向其他類型轉換 operator int() const { return val; }; private: std::size_t val; }; SmallInt si; si = 4; // 先將4隱式轉換成SmallInt對象, 再調用SmallInt::operator= si + 3; // 先將si隱式轉換成int, 在執行整數加法
盡管編譯器一次只能執行一個用戶定義的類型轉換, 但是隱式的用戶定義類型轉換可以置於一個標准(內置)類型轉換之前或之后並與其一起使用. 如
SmallInt si = 3.14; // 首先內置類型double轉換為int, 然后執行隱式的轉換 si + 3.14; // SmallInt類型轉換運算符將si轉換成int, 內置類型將所得的int繼續轉換成double
因為類型轉換運算符是隱式執行的, 所以無法給這些函數傳遞實參, 當然也就不能在類型轉換運算符的定義中使用任何形參.
盡管類型轉換函數不負責制定返回類型, 但實際上每個類型轉換函數都會返回一個對應類型的值.
class SmallInt{ public: int operator int() const; // 錯誤, 指定了返回類型 operator int(int i = 0) const; // 錯誤, 參數列表不為空 operator int*() const {return 42;} // 錯誤, 42不是int型的指針 operator int() const { return val; }; private: std::size_t val; };
類型轉換運算符可能產生意外結果
int i = 42; cin << i; // 如果istream含有向bool的類型轉換且該轉換不是顯式的時, 則該代碼在編譯器看來是合法的!!!!
istream的bool類型轉換運算符將cin轉換成bool, 這個bool接着被提升為int並作用於內置的左移運算符的左側運算對象. 這樣一來, 提升后的bool值被左移42個位置.
顯式的類型轉換運算符
在類型轉換函數之前加上explicit修飾符. 如
class SmallInt{ public: explicit operator int() const { return val; }; }; SmallInt si = 4; // 正確, 構造函數不是顯式的 si + 3; // 錯誤, 類型轉換構造函數是顯式的 static_cast<int>(si) + 3; // 正確, 強制類型轉換
該規定存在一個例外, 即如果表達式被用作條件, 則編譯器會將顯式的類型轉換自動應用於它, 顯式的類型轉換將被隱式地執行.
無論在什么時候在條件中使用流對象, 都會使用為IO類定義的operator bool, 如:
while (std::cin >> value)
while語句條件執行輸入運算符, 它負責將數據讀入到value並且返回cin. cin被istream operator bool類型轉換函數隱式執行了轉換.
向bool類型的轉換通常用在條件部分, 因此operator bool一般定義成explicit的, 使得在條件部分可以隱式執行, 而在其他部分則需要顯式請求.
9.2 避免有二義性的類型轉換
如果類中包含一個或多個類型轉換, 則必須確保在類類型和目標類型之間只存在唯一一種轉換方式. 否則很可能引發二義性.
在兩種情況下會產生多重轉換路徑:
兩個類提供了相同的類型轉換. 如類A定義了一個接受B類對象的轉換構造函數, 同時B類定義了一個轉換目標是A類的類型轉換運算符, 我們就說它們提供了相同的類型轉換.
類定義了多個轉換規則, 而這些轉換涉及的類型本身可以通過其他類型轉換聯系在一起. 最典型的是算術運算符, 對一個給定的類來說, 最好只定義最多一個與算術類型有關的轉換規則.
實參匹配和相同類型的轉換
下面的例子定義了兩種將B轉換成A的方法: 一種用B的類型轉換運算符, 一種用A的以B為參數的轉換構造函數.
class A{ A() = default; A(const &B); // 隱式轉換構造函數, 將一個B轉換為A //其他成員 }; class B{ operator A() const; // 類型轉換運算符, 將一個B轉換成A //其他成員 }; A f(const A&); B b; A a = f(b); // 二義性錯誤, 含義是f(B::operator A())還是f(A::A(const B&))? // 要想執行上述調用, 就不得不顯式地調用類型轉換運算符或者轉換構造函數 A a1 = f(b.operator A()) // 使用B的類型轉換運算符 A a2 = f(A(b)) // 使用A的構造函數
二義性與轉換目標為內置類型的多重類型轉換
如果轉換源(或者轉換目標)類型本身可以通過其他類型轉換聯系在一起, 則同樣會產生二義性的問題. 比如定義了多個算術類型的類型轉換運算符.
舉例: 略
重載函數與轉換構造函數
如果兩個或多個類型提供了同一種可行匹配, 則這些類型轉換一樣好.
struct C{ C(int); }; struct D{ D(int); }; void manip(const C&); void manip(const D&); manip(10); // 二義性錯誤, 含義是manip(C(10))還是manip(D(10)). manip(C(10)); // 正確
重載函數與用戶定義的類型轉換
當調用重載函數時, 如果兩個(或多個)用戶定義的類型轉換都提供了可行的匹配, 則認為這些類型轉換一樣好.
struct E{ E(double); }; void manip2(const C&); void manip2(const E&); manip2(10); // 二義性錯誤, 兩個不同的用戶定義的類型轉換都能在此處用.
9.3 函數匹配與重載運算符
當運算符函數出現在表達式中時, 候選函數集的規模要比我們使用調用運算符調用函數時更大. 如果a是一種類類型, 則表達式a sym b可能是
a.operatorsym(b) // a有一個operatorsym成員函數 operatorsym(a, b) // operatorsym是一個普通函數
與普通函數不同, 我們不能通過調用的形式來區分當前調用的是成員函數還是非成員函數.
表達式中運算符的候選函數集既應該包括成員函數, 也應該包括非成員函數.
如果我們對同一個類既提供轉換目標是算術類型的類型轉換, 也提供了重載的運算符, 則將會遇到重載運算符與內置運算符的二義性問題.