Effective Modern C++ 條款6 當auto會推斷出不合理的類型時使用顯式類型初始化語法

當auto會推斷出不合理的類型時使用顯式類型初始化語法

條款5中我們說明了使用auto聲明類型會比顯式聲明類型更好,但是有時候auto類型推斷會和我們想象中有點差別。例如,我有一個函數以Widget 爲參數,返回std::vector<bool>,容器中每個bool表示Widget是否提供特別的性質:
std::vector<bool> features(const Widget &w);
然後,如果第5個元素代表Widget的高屬性的話,我們可以寫出下面的代碼:

Widget w;
...
bool highPriority = features(w)[5];  // w是否具有高屬性?
...
processWidget(w, highPriority);  // 根據w的高屬性來處理它

這代碼沒有錯誤,它可以運行得很好。但是如果我們做一個看似無關緊要的改變,用auto聲明代替顯式類型聲明:
auto highPriority = features(w)[5];

現在情況改變了,代碼編譯可以通過,但是代碼不會像預期那樣運行了:
processWidget(w,highPriority); // 未定義行爲

就像註釋所說的那樣,processWidget函數現在的行爲是未定義的。爲什麼呢?答案可能會讓你感到驚訝,在auto的那代碼中,highPriority 變量的類型不再是bool類型了。儘管std::vector<bool>概念上是持有bool類型,但是std::vector<bool>operator[]函數返回的並不是容器中元素的引用(除了std::vector<bool>std::vector::operator[]都會返回容器內元素的引用)。與之代替的是,它返回std::vector<bool>::reference類型(std::vector<bool>中的嵌套類)。

std::vector<bool>::reference的存在是因爲std::vector<bool>容器是以位(bit)的方式來存儲bool變量。這就引起了operator[]函數出現問題,因爲std::vector<T>operator[]函數應該是返回T&類型,但是C++無法引用位bit。因此std::vector<bool>operator[]函數無法返回bool&,只能返回一個行爲類似(acks like)bool&的對象,這個對象必須可以用於所有需要bool&的地方。在這些情況下,std::vector<bool>::reference會隱式轉換爲bool類型(不是bool&)。

請記住這個特性,然後重新看看我們一開始的代碼:
bool highPriority = features(w)[5];

features 函數返回一個std::vector<bool>對象,然後該對象調用了operator[]函數,這個函數返回一個std::vector<bool>::reference對象,接着該對象隱式轉換爲一個bool類型變量來初始化highPriority。最終highPriority 的值爲features 函數返回的std::vector<bool>對象的第五個元素的值,就是代碼看上去那樣。

對比用auto聲明的highPriority
auto highPriority = features(w)[5]; // 類型推斷

同樣地,features函數返回一個std::vector<bool>對象,然後該對象調用了operator[]函數,這個函數返回一個std::vector<bool>::reference對象,但這次不一樣,因爲highPriority 的類型是通過auto推斷的,highPriority 的值不再是features 函數返回的std::vector<bool>對象的第五個元素的值了。

highPriority 的值取決於std::vector<bool>::reference的實現。一種實現是該對象內有一個指向容器內字(word)數據結構的偏移量特定個位(bit)數的指針。

feature 函數返回的是個臨時的std::vector<bool>對象,這個對象沒有名字,爲了方便討論我們稱它爲temptemp調用了它的operator[]函數,返回的std::vector<bool>::reference對象包含了一個指向字(word)的偏移量爲5的指針。所以highPriority 是這個std::vector<bool>::reference對象的拷貝,那麼highPriority內也有一個指針,指向temp中的字數據(word)偏移量爲5的位。當聲明定義highPriority結束後,temp被析構,那麼highPriority中的指針就變成了空懸指針(dangling pointer),這就造成了調用processWidget 時產生未定義行爲:
processWidget(w, highPriority); // 未定義行爲
// highPriority 內含有空懸指針

std::vector<bool>::reference是代理類(proxy class)的一個例子:一個類的存在是爲了模仿和增強另一個類的行爲。代理類在很多情況下被使用,例如,std::vector<bool>::reference的存在是爲了給你一個假象,讓你以爲std::vector<bool>operator[]函數返回的是位bit的引用;再比如說標準庫中的智能指針接收原生指針的內存管理。代理類的使用是根深蒂固的,事實上,代理模式也是一個長時間被褒獎的設計模式。

一些代理類的設計是被用戶使用的,例如std::shared_ptrstd::unique_ptr,而另一些代理類的設計是默默工作的,例如std::vector<bool>::reference,它的同胞是std::bitset::reference

與它們同一陣營的還有C++庫中一些使用表達式模板(expression templates)的類。開發這些庫文件是爲了寫出高效數字化的代碼。例如,一個矩陣類Matrix 以及4個Matrix 對象m1, m2, m3, m4,給定下面的表達式:
Matrix sum = m1 + m2 + m3 + m4;

如果Matrix 對象的operator+函數返回的是一個代理類,而不是返回Matrix,那麼計算的效率可以高很多,比如operator+函數返回一個像Sum<Matrix,Matrix>的代理類來替代Matrix。就像std::vector<bool>::referencebool那樣,代理類也可以隱式轉換爲Matrix,也可以通過“=”來初始化sum(本人不懂模板元編程)。

總結一條規則,auto應付不好那些不想被用戶知道的代理類。這裏代理類的對象通常都是聲明表達式結束後就析構(作爲臨時變量),所以創建這種類型的變量是背離了這些庫設計的初衷。例如std::vector<bool>::reference這個例子就違反了這個初衷,從而引發了未定義行爲。

所以你應該避免這樣的代碼:
auto someVar = expression of "invisible" proxy class type;

但是你怎麼知道哪些是代理類呢,程序是不會顯示它們的實體的,他們應該是不可見的,至少在概念上。就算你真的發現了他們,那麼你真的會放棄條款5所說的auto的優勢嗎?

我們先來解決如何發現它們這個問題。儘管代理類在程序設計中是不可見的,但是庫文件會有文檔告知。你對你使用的庫文件的設計越熟悉,你使用庫中代理類的遇到的坑就越少。

如果庫文檔很讓人失望,那麼看頭文件來彌補。源代碼完全遮掩代理類是很少可能發生的,代理類通常是因爲客戶調用某個函數而返回的,所以調用函數的簽名會反應出它們的實體。例如,這是std::vector<bool>::operator[]的說明:

namespace std {
  template <class Allocator>
  class vector<bool, Allocator> {
  public:
  ...
  class reference { ... };
  reference operator[](size_type n);
  ...
  };
}

假定你已經知道std::vector<T>operator[]函數正常會返回T&,那麼這種非常規的返回一個代理類會是一種警告。仔細留意你使用的接口,你會經常發現代理類的實體。

在實踐中,大部分開發者只會在追查模糊的編譯錯誤或者修改單元測試的bug時纔會發現代理類型的使用。不管你是怎麼發現它們的,我想說的只要auto推斷出的類型是代理類,而不是代理類所代理的類,那麼解決辦法不一定需要放棄使用autoauto本身是沒有問題的,問題只是auto沒有推斷出我們想要的類型。解決辦法是強迫進行不一樣的類型推斷,這種方法我稱爲顯式類型初始化語法(explicitly typed initializer idiom)

顯式類型初始化語法用auto聲明變量,但是初始化表達式顯式說明你想要auto推斷的類型。下面這個例子說明它如何強迫highPriority 推斷爲bool類型:
auto highPriority = static_cast<bool>(features(w)[5]);

雖然features(w)[5]還是會返回std::vector<bool>::reference類型,但是類型轉換使得表達式的類型轉換爲bool,然後autohighPriority 的類型推斷爲bool

在矩陣Matrix 的那個例子中,我們可以這樣寫:
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

這種語法的應用不限於初始值爲代理類這種情況。這語法還可以用於你故意創建一個與初始表達式類型不一樣的變量。例如,你有個函數計算可容忍誤差值:
double calcEpsilon();

這個函數明確返回double,但是你知道你的應用程序只需要float精確度就足夠了,可是值得擔心doublefloat的所佔字節大小不同。你可以聲明一個float變量來存儲calcEpsilon函數的結果,
float ep = calcEpsilon(); // 隱式轉換

但是這樣是很難告訴他人:”我是故意減少函數返回結果的精度的“。但是用顯示類型初始化語法可以做到這個意思:
auto ep = static_cast<float>(calcEpsilon());

同樣地,你也可以使用它當你故意把float的值附給整形數。再比如,一個double值,範圍是0.0~1.0,代表需要的元素與容器首元素的距離(0.5爲容器中間的元素),也就是說你需要計算具有隨機訪問迭代器的容器(例如,std::vectorstd::deque)中的元素的索引。再進一步說,你需要的索引結構類型是int,那麼假如容器爲cdouble值爲d,那麼你可以計算索引:
int index = d * c.size();
但是你故意將右邊的double轉換爲int這個意圖是模糊的,使用顯示類型初始化語義可以清楚地表達這個意圖:
auto index = static_cast<int>(d * c.size());

總結

需要記住的2點:

  • 不可見的代理類初始表達式可能會使auto推斷出錯誤的結果
  • 顯示類型初始化語法迫使auto推斷你想要的類型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章