天天看點

C++中潛在的二義性

   C++有一種思想:它認為潛在的二義性不是一種錯誤。

這是潛在二義性的一個例子:

class B;                    // 對類B提前聲明

class A {

public:

  A(const B&);              // 可以從B構造而來的類A

};

class B {

public:

  operator A() const;       // 可以從A轉換而來的類B

};

 這些類的聲明沒一點錯——它們可以在相同的程式中共存而沒一點問題。但是,看看下面,當把這兩個類結合起來使用,在一個輸入參數為A的函數裡實際傳進了一個B的對象,這時将會發生什麼呢?

void f(const A&);

B b;

f(b);                       // 錯誤!——二義

 一看到對f的調用,編譯器就知道它必須産生一個類型A的對象,即使有一個類型B的對象。有兩種都很好的方法來實作。一種方法是調用類A的構造函數,它以b為參數構造一個新的A的對象。另一種方法是調用類B裡自定義的轉換運算符,它将b轉換成一個A的對象。因為這兩個途徑都一樣可行,編譯器拒絕從它們中選擇一個。

 在沒碰上二義的情況下,程式可以使用。這正是潛在的二義所具有的潛伏的危害性。它可以長時期地潛伏在程式裡,不被發覺也不活動;一旦不知情的程式員真的做了什麼具有二義性的操作,混亂就會爆發。這導緻有這樣一種令人擔心的可能:釋出了一個函數庫,它可以在二義的情況下被調用,卻不知道自己正在這麼做。

另一種類似的二義的形式源于C++語言的标準轉換——甚至沒有涉及到類:

void f(int);

void f(char);

double d = 6.02;

f(d);                         // 錯誤!——二義

 d是該轉換成int還是char呢?兩種轉換都可行,是以編譯器幹脆不去做結論。幸運的是,可以通過顯式類型轉換來解決這個問題:

f(static_cast<int>(d));       // 正确, 調用f(int)

f(static_cast<char>(d));      // 正确, 調用f(char)

 多繼承充滿了潛在二義性的可能。最常發生的一種情況是當一個派生類從多個基類繼承了相同的成員名時:

class Base1 {

public:

  int doIt();

};

class Base2 {

public:

  void doIt();

};

class Derived: public Base1,     // Derived沒有聲明

               public Base2 {    // 一個叫做doIt的函數

  ...

};

Derived d;

d.doIt();                   // 錯誤!——二義

 當類Derived繼承兩個具有相同名字的函數時,C++沒有認為它有錯,此時二義隻是潛在的。然而,對doIt的調用迫使編譯器面對這個現實,除非顯式地通過指明函數所需要的基類來消除二義,函數調用就會出錯:

d.Base1::doIt();            // 正确, 調用Base1::doIt

d.Base2::doIt();            // 正确, 調用Base2::doIt

 這不會令很多人感到麻煩,但當看到上面的代碼沒有用到通路權限時,一些本來很安分的人會動起心眼想做些不安分的事:

class Base1 { ... };        // 同上

class Base2 {

private:

  void doIt();              // 此函數現在為private

};                          

class Derived: public Base1, public Base2

{ ... };                    // 同上

Derived d;

int i = d.doIt();           // 錯誤! — 還是二義!

 對doIt的調用還是具有二義性,即使隻有Base1中的函數可以被通路。另外,隻有Base1::doIt傳回的值可以用于初始化一個int這一事實也與之無關——調用還是具有二義性。如果想成功地調用,就必須指明想要的是哪個類的doIt。

 C++中有一些最初看起來會覺得很不直覺的規定,現在就是這種情況。具體來說,為什麼消除“對類成員的引用所産生的二義”時不考慮通路權限呢?有一個非常好的理由,它可以歸結為: 改變一個類成員的通路權限不應該改變程式的含義。

 比如前面那個例子,假設它考慮了通路權限。于是表達式d.doIt()決定調用Base1::doIt,因為Base2的版本不能通路。現在假設Base1的Doit版本由public改為protected,Base2的版本則由private改為public。

 轉瞬之間,同樣的表達式d.doIt()将導緻另一個完全不同的函數調用,即使調用代碼和被調用函數本身都沒有被修改!這很不直覺,編譯器甚至無法産生一個警告。可見,不是象當初所想的那樣,對多繼承的成員的引用要顯式地消除二義性是有道理的。

既然寫程式和函數庫時有這麼多不同的情況會産生潛在的二義性,那麼,一個好的軟體開發者該怎麼做呢?最根本的是,一定要時時小心它。想找出所有潛在的二義性的根源幾乎是不可能的,特别是當程式員将不同的獨立開發的庫結合起來使用時,但在了解了導緻經常産生潛在二義性的那些情況後,就可以在軟體設計和開發中将它出現的可能性降到最低。