【C++】C++中重載運算符和類型轉換

重載運算符是具備特殊名字的函數: 它們的名字由關鍵字operator和其後要定義的運算符號共同組成.
重載運算符函數的參數數量與該運算符做用的運算對象數量同樣多.
對於二元運算符來講, 左側運算對象傳遞給第一個參數, 而右側運算對象傳遞給第二個參數.
除了重載的函數調用運算符operator()除外, 其餘重載運算符不能含有默認實參.
若是一個運算符函數是成員函數, 則它的第一個(左側)運算對象綁定到隱式的this指針上, 所以成員運算符函數的顯式參數數量比運算符的運算對象總數少一個.
對於一個運算符函數來講, 它或者是類的成員, 或者至少有一個類類型的參數. 如:

算法

int operator+(int, int);   // 錯誤, 不能爲int重定義內置的運算符

這意味着當運算符做用於內置類型時, 咱們沒法改變運算符的含義.
只能重載已有的運算符, 而無權發明新的運算符號, 對於重載的運算符來講, 其優先級和結合律與對應的內置運算符保持一致.
不能被重載的運算符: "::" ".*" "." "? :"
通常狀況下不該該被重載的運算符: 逗號, 取地址, 邏輯與和邏輯或運算符.

非成員運算符函數的調用:數組

data1 + data2; // 普通的表達式
operator+(data1, data2); // 等價的函數調用

成員運算符函數的調用:ide

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.
賦值運算符應該返回它左側運算對象的一個引用.ui

 

1 輸入輸出運算符

1.1 重載輸出運算符

一般狀況下, 輸出運算符的第一個形參是很是量ostream對象的引用. 之因此ostream是很是量是由於向流寫入內容會改變其狀態, 形參是引用是由於咱們沒法拷貝ostream.
第二個形參通常來講是一個常量引用, 該常量是咱們想要打印的類類型. 是引用的緣由是由於咱們但願避免賦值實參, 爲常量是由於打印一般不會改變對象的內容.
爲了與其餘輸出運算符保持一致, operator<<通常要返回它的ostream形參.

this

ostream &operator<<(ostream &os, const Sales_data &item)
{
    os << item.isbn() << "  " << item.units_sold << "  "
       << item.revenue << "  " << item.avg_price();
    return os;
}

一般輸出運算符只負責打印內容而非控制格式, 輸出運算符不該該打印換行符.

輸入輸出運算符必須是普通的非成員函數, 而不能是類的成員函數. 不然它的左側運算對象將是咱們的類的一個對象.
若是是類的成員的話, 則第一個參數就必須是類的對象, 可是輸入輸出運算符的左側運算對象(第一個參數)是ostream對象.
IO運算符一般須要讀寫類的非公有數據成員, 因此IO運算符通常被聲明爲友元.spa

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 算術和關係運算符

一般狀況下把算術和關係運算符定義成非成員函數以容許對左側或右側的運算對象進行轉化, 由於這些運算符通常不須要改變運算對象的狀態, 因此形參都是常量引用.

code

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是一個普通函數

與普通函數不一樣, 咱們不能經過調用的形式來區分當前調用的是成員函數仍是非成員函數.表達式中運算符的候選函數集既應該包括成員函數, 也應該包括非成員函數.若是咱們對同一個類既提供轉換目標是算術類型的類型轉換, 也提供了重載的運算符, 則將會遇到重載運算符與內置運算符的二義性問題.

相關文章
相關標籤/搜索