2017年6月3日 星期六

隱式轉換 執行的條件 與 如何避免

隱式轉換

tags: C++ Concept2
More Effective C++ 條款5
在不同的型別之間允許執行單引數與轉型運算子的隱式轉換
編譯器會想盡辦法為你找出一個適當的的函式

什麼是引數與參數

參數指的是函式定義或宣告上的,比如說
void fun(int a){...}
這個A稱為 fun() 的第一個參數
引數指的是使用的時候比如說
int num=1;
fun(num);
這時候這裡的 num 的位置稱為第一個引數

型態不符

當引數型態與參數不符的時候
假設有一個函式的參數為 string
void funStr(string s){}
使用時輸入的引數是 char[]
char str[] = "ABC";
funStr(str);
這時候引數(char[])與參數(string)型態不符
編譯器會替你找出一個適合的建構子或轉型運算子轉換。

轉換函式

有兩個會成為轉換函式
  • 單引數建構子
  • 轉型運算子

單引數建構子

上面的例子他發現 string (const char* s); 這個很適合,而且沒有第二個選擇,編譯器會自做主張的幫你修改。
funStr(string(str));
這裡可以參考 string 的 ref 其中第4個建構子就是被選中的函式。
http://www.cplusplus.com/reference/string/string/string/
// from c-string (4)
string (const char* s);
任何一個單引數的建構子都會成為成為隱式轉換的函式
(語意上來看基本型別也可以算在這裡。如:int i(0);)
這裡指的是引數不是參數,限制比較寬一些
以下的建構子都算是單引數,都會成為隱式轉換的函式
struct A{
    A(int i){}
};

struct B{
    B(int i, int j=0){}
};
一個具有預設指定的參數並不影響單引數,因為使用的時候可以不輸入。
A a(0);
B b(0);

轉型運算子

還有另一種情況是轉型運算子,假設我們有一個自訂型別內含一個轉型運算子。
struct A {
    A() {}
    operator string();
};
這個運算子用法可以有兩種方式呼叫
A num;

// C++ 轉型
static_cast<string>(num);
// C style 轉型
(string)num;
現在有一個函式如下
void funStr(string s) {}
如果你這樣使用
funStr(str);
編譯器會為你找出轉型運算子自動套用
funStr((string)str);

如果有兩個適合的隱式轉換函式

會發生曖昧(ambiguous)衝突,編譯器無法幫你做出決定。
有兩個類別與一個函式的定義如下
struct A{
    A(class B& i){}
};
struct B{
    B(){}
    operator A(){return B();}
};
void fun(A i){}
當你這樣使用的時候
B b;
fun(b);
error: conversion from 'B' to 'A' is ambiguous
這時候有兩個候選分別是 A 的建構子與 B 的轉型運算子,適用於這裡,編譯器沒辦法幫你做出取捨。

如何避免這個問題(拒絕編譯器隱式轉換)

明確的表達

為你的單引數建構子或轉型運算子加上 explicit 關鍵字
struct A {
    A() {}
    explicit operator string();
};
如此一來除非你明顯的告訴編譯器
A a;
(string)a;
需要轉型否則編譯器將不會自作主張的幫你轉換
用在建構子上也是直接加上即可。

替身類別

用一個類別轉介也可以達到相同的效果
struct Num {
    struct proxy{
        proxy(int i): len(i){}
        operator int&(){return len;}
        int len;
    };
    Num(proxy i): num(i) {}
    int num;
};
void fun(Num n){}
阻斷編譯器的隱式轉換,如此一來你只能明確的輸入正確的型別。
fun(Num(0));
因為它們是高度相關的,而且不應該被客戶使用,應該把它放入類內並使用私有保護。

使用函式轉換型別

明確的使用函式來轉換型別,而不使用隱式轉換運算子
如 string 內的函式 c_str() 就是一個應用的實例
依據自己想轉出的型別做適當的函式,必要時可以加上 const 避免在交出成員指針之後,成員資料非適當的被修改造成錯誤。

例外

某些條件下等號可以成為拒絕隱式轉換的手段,一個自訂型別宣告時如果是帶等號的會影響轉型目標。
型別定義如下
struct Str{
    Str(string s=""){}
};
現在當你使用建構子建構時
Str s("ABC");
當前型別為 char* 目標型別為建構子的 string,編譯器會嘗試將 char* 轉成 string 而 string 正好有一個引數為 char* 的單引數建構子,於是編譯器適當的幫你轉換。
Str s(string("ABC"));
當你使用等號作為建構子時
Str s = "ABC";
這時候目標型別是 Str (左右兩邊要相等),編譯器會嘗試將 char* 轉成 Str,因為找不到適合的函式,故無法轉換。(不過這在VC上不會出問題,只出現在gcc上)
等號這裡的語意是複製語意(copy initialization),之所以最終仍然直接呼叫建構子是因為,編譯器的優化—-複製省略(copy elision),使得最終的結果與直接呼較建構子(direct initialization)相似。

參考

沒有留言:

張貼留言