條款三:理解`decltype`
=========================
`decltype`是一個怪異的發明。給定一個變量名或者表達式,`decltype`會告訴你這個變量名或表達式的類型。`decltype`的返回的類型往往也是你期望的。然而有時候,它提供的結果會使開發者極度抓狂而不得參考其他文獻或者在線的Q&A網站。
我們從在典型的情況開始討論,這種情況下`decltype`不會有令人驚訝的行為。與`templates`和`auto`在類型推導中行為相比(請見條款一和條款二),`decltype`一般只是復述一遍你所給他的變量名或者表達式的類型,如下:
```cpp
const int i = 0; // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point{
int x, y; // decltype(Point::x) is int
};
Widget w; // decltype(w) is Widget
if (f(w)) ... // decltype(f(w)) is bool
template<typename T> // simplified version of std::vector
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) is vector<int>
...
if(v[0] == 0) // decltype(v[0]) is int&
```
看到沒有?毫無令人驚訝的地方。
在C++11中,`decltype`最主要的用處可能就是用來聲明一個函數模板,在這個函數模板中返回值的類型取決于參數的類型。舉個例子,假設我們想寫一個函數,這個函數中接受一個支持方括號索引(也就是"[]")的容器作為參數,驗證用戶的合法性后返回索引結果。這個函數的返回值類型應該和索引操作的返回值類型是一樣的。
操作子`[]`作用在一個對象類型為`T`的容器上得到的返回值類型為`T&`。對`std::deque`一般是成立的,例如,對`std::vector`,這個幾乎是處處成立的。然而,對`std::vector<bool>`,`[]`操作子不是返回`bool&`,而是返回一個全新的對象。發生這種情況的原理將在條款六中討論,對于此處重要的是容器的`[]`操作返回的類型是取決于容器的。
`decltype`使得這種情況很容易來表達。下面是一個模板程序的部分,展示了如何使用`decltype`來求返回值類型。這個模板需要改進一下,但是我們先推遲一下:
```cpp
template<typename Container, typename Index> // works, but
auto authAndAccess(Container& c, Index i) // requires
-> decltype(c[i]) // refinements
{
authenticateUser();
return c[i];
}
```
將`auto`用在函數名之前和類型推導是沒有關系的。更精確地講,此處使用了`C++11`的尾隨返回類型技術,即函數的返回值類型在函數參數之后聲明(“->”后邊)。尾隨返回類型的一個優勢是在定義返回值類型的時候使用函數參數。例如在函數`authAndAccess`中,我們使用了`c`和`i`定義返回值類型。在傳統的方式下,我們在函數名前面聲明返回值類型,`c`和`i`是得不到的,因為此時`c`和`i`還沒被聲明。
使用這種類型的聲明,`authAndAccess`的返回值就是`[]`操作子的返回值,這正是我們所期望的。
`C++11`允許單語句的`lambda`表達式的返回類型被推導,在`C++14`中之中行為被拓展到包括多語句的所有的`lambda·表達式和函數。在上面`authAndAccess`中,意味著在`C++14`中我們可以忽略尾隨返回類型,僅僅保留開頭的`auto`。使用這種形式的聲明,
意味著將會使用類型推導。特別注意的是,編譯器將從函數的實現來推導這個函數的返回類型:
```cpp
template<typename Container, typename Index> // C++14;
auto authAndAccess(Container &c, Index i) // not quite
{ // correct
authenticateUser();
return c[i];
} // return type deduced from c[i]
```
<font color='#990000'>條款二</font>解釋說,對使用`auto`來表明函數返回類型的情況,編譯器使用模板類型推導。但是這樣是回產生問題的。正如我們所討論的,對絕大部分對象類型為`T`的容器,`[]`操作子返回的類型是`&T`, 然而<font color='#990000'>條款一</font>提到,在模板類型推導的過程中,初始表達式的引用會被忽略。思考這對下面代碼意味著什么:
```cpp
std::deque<int> d;
...
authAndAccess(d, 5) = 10; // authenticate user, return d[5],
// then assign 10 to it;
// this won't compile!
```
此處,`d[5]`返回的是`int&`,但是`authAndAccess`的`auto`返回類型聲明將會剝離這個引用,從而得到的返回類型是`int`。`int`作為一個右值成為真正的函數返回類型。上面的代碼嘗試給一個右值`int`賦值為10。這種行為是在`C++`中被禁止的,所以代碼無法編譯通過。
為了讓`authAndAccess`按照我們的預期工作,我們需要為它的返回值使用`decltype`類型推導,即指定`authAndAccess`要返回的類型正是表達式`c[i]`的返回類型。`C++`的擁護者們預期到在某種情況下有使用`decltype`類型推導規則的需求,并將這個功能在`C++14`中通過`decltype(auto)`實現。這使這對原本的冤家(`decltype`和`auto`)在一起完美地發揮作用:`auto`指定需要推導的類型,`decltype`表明在推導的過程中使用`decltype`推導規則。因此,我們可以重寫`authAndAccess`如下:
```cpp
template<typename Container, typename Index> // C++14; works,
decltype(auto) // but still
authAndAccess(Container &c, Index i) // requires
{ // refinement
authenticateUser();
return c[i];
}
```
現在`authAndAccess`的返回類型就是`c[i]`的返回類型。在一般情況下,`c[i]`返回`T&`,`authAndAccess`就返回`T&`,在不常見的情況下,`c[i]`返回一個對象,`authAndAccess`也返回一個對象。
`decltype(auto)`并不僅限使用在函數返回值類型上。當時想對一個表達式使用`decltype`的推導規則時,它也可以很方便的來聲明一個變量:
```cpp
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction
// myWidget1's type is Widget
decltype(auto) myWidget2 = cw // decltype type deduction:
// myWidget2's type is
// const Widget&
```
我知道,到目前為止會有兩個問題困擾著你。一個是我們前面提到的,對`authAndAccess`的改進。我們在這里討論。
再次看一下`C++14`版本的`authAndAccess`的聲明:
```cpp
template<typename Container, typename Index>
decltype(auto) anthAndAccess(Container &c, Index i);
```
這個容器是通過非`const`左值引用傳入的,因為通過返回一個容器元素的引用是來修改容器是被允許的。但是這也意味著不可能將右值傳入這個函數。右值不能和一個左值引用綁定(除非是`const`的左值引用,這不是這里的情況)。
誠然,傳遞一個右值容器給`authAndAccess`是一種極端情況。一個右值容器作為一個臨時對象,在 `anthAndAccess` 所在語句的最后被銷毀,意味著對容器中一個元素的引用(這個引用通常是`authAndAccess`返回的)在創建它的語句結束的地方將被懸空。然而,這對于傳給`authAndAccess`一個臨時對象是有意義的。一個用戶可能僅僅想拷貝一個臨時容器中的一個元素,例如:
```cpp
std::deque<std::string> makeStringDeque(); // factory function
// make copy of 5th element of deque returned
// from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);
```
支持這樣的應用意味著我們需要修改`authAndAccess`的聲明來可以接受左值和右值。重載可以解決這個問題(一個重載負責左值引用參數,另外一個負責右值引用參數),但是我們將有兩個函數需要維護。避免這種情況的一個方法是使`authAndAccess`有一個既可以綁定左值又可以綁定右值的引用參數,條款24將說明這正是統一引用(`universal reference`)所做的。因此`authAndAccess`可以像如下聲明:
```cpp
template<typename Container, typename Index> // c is now a
decltype(auto) authAndAccess(Container&& c, // universal
Index i); // reference
```
在這個模板中,我們不知道我們在操作什么類型的容器,這也意味著我們等同地忽略了它用到的索引對象的類型。對于一個不清楚其類型的對象使用傳值傳遞通常會冒一些風險,比如因為不必要的復制而造成的性能降低,對象切片的行為問題,被同事嘲笑,但是對容器索引的情況,正如一些標準庫的索引(`std::string, std::vector, std::deque`的`[]`操作)按值傳遞看上去是合理的,因此對它們我們仍堅持按值傳遞。
然而,我們需要更新這個模板的實現,將`std::forward`應用給統一引用,使得它和條款25中的建議是一致的。
```cpp
template<typename Container, typename Index> // final
decltype(auto) // C++14
authAndAccess(Container&& c, Index i) // version
{
authenticateUser();
return std::forward<Container>(c)[i];
}
```
這個實現可以做我們期望的任何事情,但是它要求使用支持`C++14`的編譯器。如果你沒有一個這樣的編譯器,你可以使用這個模板的`C++11`版本。它出了要你自己必須指定返回類型以外,和對應的`C++14`版本是完全一樣的,
```cpp
template<typename Container, typename Index> // final
auto // C++11
authAndAccess(Container&& c, Index i) // version
-> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
```
另外一個容易被你挑刺的地方是我在本條款開頭的那句話:`decltype`幾乎所有時候都會輸出你所期望的類型,但是有時候它的輸出也會令你吃驚。誠實的講,你不太可能遇到這種以外,除非你是一個重型庫的實現人員。
為了徹底的理解`decltype`的行為,你必須使你自己對一些特殊情況比較熟悉。這些特殊情況太晦澀難懂,以至于很少有書會像本書一樣討論,但是同時也可以增加我們對`decltype`的認識。
對一個變量名使用`decltype`得到這個變量名的聲明類型。變量名屬于左值表達式,但這并不影響`decltype`的行為。然而,對于一個比變量名更復雜的左值表達式,`decltype`保證返回的類型是左值引用。因此說,如果一個非變量名的類型為`T`的左值表達式,`decltype`報告的類型是`T&`。這很少產生什么影響,因為絕大部分左值表達式的類型有內在的左值引用修飾符。例如,需要返回左值的函數返回的總是左值引用。
這種行為的意義是值得我們注意的。但是在下面這個語句中
```cpp
int x = 0;
```
`x`是一個變量名,因此`decltyper(x)`是`int`。但是如果給`x`加上括號"(x)"就得到一個比變量名復雜的表達式。作為變量名,`x`是一個左值,同時`C++`定義表達式`(x)`也是左值。因此`decltype((x))`是`int&`。給一個變量名加上括號會改變`decltype`返回的類型。
在`C++11`中,這僅僅是個好奇的探索,但是和`C==14`中對`decltype(auto)`支持相結合,函數中返回語句的一個細小改變會影響對這個函數的推導類型。
```cpp
decltype(auto) f1()
{
int x = 0;
...
return x; // decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{
int x = 0;
return (x); // decltype((x)) is int&, so f2 return int&
}
```
`f2`不僅返回值類型與`f1`不同,它返回的是對一個局部變量的引用。這種類型的代碼將把你帶上一個為定義行為的快速列車-你完全不想登上的列車。
最主要的經驗教訓就是當使用`decltype(auto)`時要多留心一些。被推導的表達式中看上去無關緊要的細節都可能影響`decltype`返回的類型。為了保證推導出的類型是你所期望的,請使用條款4中的技術。
同時不能更大視角上的認識。當然,`decltype`(無論只有`decltype`或者還是和`auto`聯合使用)有可能偶爾會產生類型推導的驚奇行為,但是這不是常見的情況。一般情況下,`decltype`會產生你期望的類型。將`decltype`應用于變量名無非是正確的,因為在這種情況下,`decltype`做的就是報告這個變量名的聲明類型。
|要記住的東西|
|:--------- |
|`decltype`幾乎總是得到一個變量或表達式的類型而不需要任何修改|
|對于非變量名的類型為`T`的左值表達式,`decltype`總是返回`T&`|
|`C++14`支持`decltype(auto)`,它的行為就像`auto`,從初始化操作來推導類型,但是它推導類型時使用`decltype`的規則|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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