條款4:知道如何查看類型推導
==========================
對類型推導結果的查看的工具的選擇和你在軟件開發過程中的相關信息有關系。我們要探討三種可能:在你編寫代碼的時候,在編譯的時候和在運行的時候得到類型推導的信息。
###IDE編輯器
在IDE里面的代碼編輯器里面當你使用光標懸停在實體之上,常常可以顯示出程序實體(例如變量,參數,函數等等)的類型。舉一個例子,下面的代碼:
```cpp
const int theAnswer = 42;
auto x = theAnswer;
auto y = &theAnswer;
```
一個IDE的編輯器很可能會展示出`x`的推導的類型是`int`,`y`的類型是`const int*`。
對于這樣的情況,你的代碼必須處在一個差不多可以編譯的狀態,因為這樣可以使得IDE接受這種在IDE內部運行這的一個C++編譯器(或者至少是一個前端)的信息。如果那個編譯器無法能夠有足夠的能力去感知你的代碼并且parse你的代碼然后去執行類型推導,他就無法展示對應推導的類型了。
對于簡單的類型例如`int`,IDE里面的信息是正常的。但是我們隨后會發現,涉及到更加復雜的類型的時候,從IDE里面得到的信息并不一定是有幫助性的。
###編譯器診斷
一個有效的讓編譯器展示類型的辦法就是故意制造編譯問題。編譯的錯誤輸出會報告會和捕捉到的類型相關錯誤。
假設,舉個例子,我們希望看在上面例子中的`x`和`y`被推導的類型。我們首先聲明一個類模板,但是并不定義這個模板。就像下面優雅的做法:
```cpp
template<typename T> // 聲明TD
class TD; // TD == "Type Displayer"
```
嘗試實例化這個模板會導致錯誤信息,因為沒有模板的定義實現。想看`x`和`y`被推導的類型,只要嘗試去使用這些類型去實例化`TD`:
```cpp
TD<decltype(x)> xType; // 引起的錯誤
TD<decltype(y)> yType; // 包含了x和y的類型
```
我使用的變量名字的形式`variableNameType`是因為這樣有利于輸出的錯誤信息可以幫助我定位我要尋找的信息。對上面的代碼,我的一個編譯器輸出了診斷信息,其中的一部分如下:(我把我們關注的類型信息高亮了(原文中高亮了模板中的`int`和`const int*`,但是Markdown在代碼block中操作粗體比較麻煩,譯文中沒有加粗——譯者注)):
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined
另一個編譯器提供相同的信息,但是格式不太一樣:
error: 'xType' uses undefined class 'TD<int>'
error: 'yType' uses undefined class 'TD<const int *>'
排除格式的區別,我測試了所有的編譯器都會在這種代碼的技術中輸出有用的錯誤信息。
###運行時輸出
`printf`到運行的時候可以用來顯示類型信息(這并不是我推薦你使用`printf`的原因),但是它提供了對輸出格式的完全掌控。挑戰就在于你要創造一個你關心的對象的輸出的格式控制展示的textual。“這還不容易,”你會這樣想,“就是用`typeid`和`std::type_info::name`來救場啊。”在后續的對`x`和`y`的類型推導中,你可以發現你可以這樣寫:
```cpp
std::cout << typeid(x).name() << '\n'; // display types for
std::cout << typeid(y).name() << '\n'; // x and y
```
這是基于對類似于`x`或者`y`運算`typeid`可以得到一個`std::type_info`對象,`std::type_info`有一個成員函數,`name`可以提供一個C-style的字符串(也就是`const char*`)代表了類型的名字。
調用`std::type_info::name`并不會確定返回有意義的東西,但是實現上是有幫助性質的。幫助是多種多樣的。舉一個例子,GNU和Clang編譯器返回`x`的類型是“`i`”,`y`的類型是“`PKi`”。這些編譯器的輸出結果你一旦學會就可以理解他們,“`i`”意味著“`int`”,“`PK`”意味著“pointer to ~~konst~~ const”(所有的編譯器都支持一個工具,`C++filt`,它可以解析這樣的“亂七八糟”的類型。)微軟的編譯器提供更加直白的輸出:“`int`”對`x`,“`int const*`”對`y`。
因為這些結果對`x`和`y`而言都是正確的,你可能認為類型輸出的問題就此解決了,但是這并不能輕率。考慮一個更加復雜的例子:
```cpp
template<typename T> // template function to
void f(const T& param); // be called
std::vector<Widget> createVec(); // 工廠方法
const auto vw = createVec(); // init vw w/factory return
if (!vw.empty()) {
f(&vw[0]); // 調用f
…
}
```
在代碼中,涉及了一個用戶定義的類型(`Widget`),一個STL容器(`std::vector`),一個`auto`變量(`vw`),這對你的編譯器的類型推導的可視化是非常具有表現性的。舉個例子,想看到模板類型參數`T`和`f`的函數模板參數`param`。
在問題中沒有`typeid`是很直接的。在`f`中添加一些代碼去展示你想要的類型:
```cpp
template<typename T>
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // 展示T
cout << "param = " << typeid(param).name() << '\n'; // 展示param的類型
…
}
```
使用GNU和Clang編譯器編譯會輸出如下結果:
T = PK6Widget
param = PK6Widget
我們已經知道對于這些編譯器,`PK`意味著“pointer to `const`”,所以比較奇怪的就是數字6,這是在后面跟著的類的名字(`Widget`)的字母字符的長度。所以這些編譯器就告我我們`T`和`param`的類型都是`const Widget*`。
微軟的編譯器輸出:
T = class Widget const *
param = class Widget const *
三種不同的編譯器都產出了相同的建議性信息,這表明信息是準確的。但是更加仔細的分析,在模板`f`中,`param`的類型是`const T&`。`T`和`param`的類型是一樣的難道不會感到奇怪嗎?舉個例子,如果`T`是`int`,`param`的類型應該是`const int&`——根本不是相同的類型。
悲劇的是,`std::type_info::name`的結果并不可靠。在這種情況下,舉個例子,所有的三種編譯器報告的`param`的類型都是不正確的。更深入的話,它們本來就是不正確的,因為`std::type_info::name`的特化指定了類型會被當做它們被傳給模板函數的時候的按值傳遞的參數。正如條款1所述,這就意味著如果類型是一個引用,他的引用特性會被忽略,如果在忽略引用之后存在`const`(或者`volatile`),它的`const`特性(或者`volatile`特性)會被忽略。這就是為什么`param`的類型——`const Widget * const &`——被報告成了`const Widget*`。首先類型的引用特性被去掉了,然后結果參數指針的`const`特性也被消除了。
同樣的悲劇,由IDE編輯器顯示的類型信息也并不準確——或者說至少并不可信。對之前的相同的例子,一個我知道的IDE的編輯器報告出`T`的類型(我不打算說):
```cpp
const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *
```
還是這個相同的IDE編輯器,`param`的類型是:
```cpp
const std::_Simple_types<...>::value_type *const &
```
這個沒有`T`的類型那么嚇人,但是中間的“...”會讓你感到困惑,直到你發現這是IDE編輯器的一種說辭“我們省略所有`T`類型的部分”。帶上一點運氣,你的開發環境也許會對這樣的代碼有著更好的表現。
如果你更加傾向于庫而不是運氣,你就應該知道`std::type_info::name`可能在IDE中會顯示類型失敗,但是Boost TypeIndex庫(經常寫做Boost.TypeIndex)是被設計成可以成功顯示的。這個庫并不是C++標準的一部分,也不是IDE和模板的一部分。更深層的是,事實上Boost庫(在[boost.com](http://boost.com/))是一個跨平臺的,開源的,并且基于一個偏執的團隊都比較喜歡的協議。這就意味著基于標準庫之上使用Boost庫的代碼接近于一個跨平臺的體驗。
這里展示了一段我們使用Boost.TypeIndex的函數`f`精準的輸出類型信息:
```cpp
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
// show T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
// show param's type
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
…
}
```
這個模板函數`boost::typeindex::type_id_with_cvr`接受一個類型參數(我們想知道的類型信息)來正常工作,它不會去除`const`,`volatile`或者引用特性(這也就是模板中的“`cvr`”的意思)。返回的結果是個`boost::typeindex::type_index`對象,其中的`pretty_name`成員函數產出一個`std::string`包含一個對人比較友好的類型展示的字符串。
通過這個`f`的實現,再次考慮之前使用`typeid`導致推導出現錯誤的`param`類型信息:
```cpp
std::vector<Widget> createVec(); // 工廠方法
const auto vw = createVec(); // init vw w/factory return
if (!vw.empty()) {
f(&vw[0]); // 調用f
…
}
```
在GNU和Clang的編譯器下面,Boost.TypeIndex輸出(準確)的結果:
T = Widget const*
param = Widget const* const&
微軟的編譯器實際上輸出的結果是一樣的:
T = class Widget const *
param = class Widget const * const &
這種接近相同的結果很漂亮,但是需要注意IDE編輯器,編譯器錯誤信息,和類似于Boost.TypeIndex的庫僅僅是一個對你編譯類型推導的一種工具而已。所有的都是有幫助意義的,但是到目前為止,沒有什么關于類型推導法則1-3的替代品。
|要記住的東西|
| :--------- |
| 類型推導的結果常常可以通過IDE的編輯器,編譯器錯誤輸出信息和Boost TypeIndex庫的結果中得到|
| 一些工具的結果不一定有幫助性也不一定準確,所以對C++標準的類型推導法則加以理解是很有必要的|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款1:理解模板類型推導
- 條款2:理解auto類型推導
- 條款3:理解decltype
- 條款4:知道如何查看類型推導
- 第二章 auto關鍵字
- 條款5:優先使用auto而非顯式類型聲明
- 條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
- 第三章 使用現代C++
- 條款7:創建對象時區分()和{}
- 條款8:優先使用nullptr而不是0或者NULL
- 條款9:優先使用聲明別名而不是typedef
- 條款10:優先使用作用域限制的enmu而不是無作用域的enum
- 條款11:優先使用delete關鍵字刪除函數而不是private卻又不實現的函數
- 條款12:使用override關鍵字聲明覆蓋的函數
- 條款13:優先使用const_iterator而不是iterator
- 條款14:使用noexcept修飾不想拋出異常的函數
- 條款15:盡可能的使用constexpr
- 條款16:保證const成員函數線程安全
- 條款17:理解特殊成員函數的生成
- 第四章 智能指針
- 條款18:使用std::unique_ptr管理獨占資源
- 條款19:使用std::shared_ptr管理共享資源
- 條款20:在std::shared_ptr類似指針可以懸掛時使用std::weak_ptr
- 條款21:優先使用std::make_unique和std::make_shared而不是直接使用new
- 條款22:當使用Pimpl的時候在實現文件中定義特殊的成員函數
- 第五章 右值引用、移動語義和完美轉發
- 條款23:理解std::move和std::forward
- 條款24:區分通用引用和右值引用
- 條款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 條款26:避免在通用引用上重定義函數
- 條款27:熟悉通用引用上重定義函數的其他選擇
- 條款28:理解引用折疊
- 條款29:假定移動操作不存在,不廉價,不使用
- 條款30:熟悉完美轉發和失敗的情況
- 第六章 Lambda表達式
- 條款31:避免默認的參數捕捉
- 條款32:使用init捕捉來移動對象到閉包
- 條款33:在auto&&參數上使用decltype當std::forward auto&&參數
- 條款34:優先使用lambda而不是std::bind
- 第七章 并發API
- 條款35:優先使用task-based而不是thread-based
- 條款36:當異步是必要的時聲明std::launch::async
- 條款37:使得std::thread在所有的路徑下無法join
- 條款38:注意線程句柄析構的行為
- 條款39:考慮在一次性事件通信上void的特性
- 條款40:在并發時使用std::atomic 在特殊內存上使用volatile
- 第八章 改進
- 條款41:考慮對拷貝參數按值傳遞移動廉價,那就盡量拷貝
- 條款42:考慮使用emplace代替insert