最近發現了《Effective Modern C++》這本書,作者正是大名鼎鼎的Scott Meyers——《Effective C++》、《Effective STL》的作者。
而就在C++11逐漸普及,甚至是C++14的新特性也進入大家的視野的時候,《Effective Modern C++》一書應運而生。此書與其前輩一樣,通過數十個條款來展開,只不過這次是集中於C++11和C++14的新特性。auto、decltype、move、lambda表達式……這些強而有力的新特性背後到底隱藏著哪些細節和要點?……
閱讀這本書的時候,感受到的豁然開朗的愉悅與初學C++時看Scott前幾本著作時別無二致。遂嘗試摘錄一二,結合所想,做些記錄,同時也試著檢查一些自己的知識點有哪些欠缺,希望大家能多多指正。
注意:
蛤蛤蛤蛤蛤
↑↑↑↑ 這樣的方框裡的片段完全不來自於原書,而是我自己的理解。
模板類型推導是C++長期以來的特性。比如:
template<typename T> void f(ParamType param); f(expr); // 調用 f
其中 ParamType 可以是和 T 有關的類型,只不過包含一些修飾,比如 const 或引用修飾符(reference qualifier)。如:
template<typename T> void f(const T& param); // ParamType 為 const T&
對於這樣的調用:
int x = 0; f(x);
一個特化(Specialize)的函數就經由類型推導生成了,T 被推導(deduce)為 int,ParamType 則被推導為 const int& 。
上面這種過程是類型推導,而
template<> void f<int>(int);就不算類型推導了——因為並沒有進行“類型推導”,而是直接指定了——cppreference上將這叫做instantiate,實例化。編譯器將特化有特定模板參數的函數模板。
在這種形式中,T 的推導不僅依賴於 expr 的類型,還和 ParamType 的形式(form)有關。對此書中給出三種情形:
(此時書中並未詳述什麼叫universal引用,不過對此情形影響不大,因為universal引用首先就不是左值引用,即不是形如 int&、T&)
在第一種情形下,類型推導有如下規則:
上兩條中,expr 指的就是函數的實參(argument),而 ParamType 是形參(parameter)的類型。書中例子為:
template<typename T> void f(T& param); int x = 27; const int cx = x; const int& rx = x; f(x); // T -> int, ParamType -> int& f(cx); // T -> const int, ParamType -> const int& f(rx); // T -> const int, ParamType -> const int&
expr,即上例的 x、cx、rx,去掉引用部分後為 int,const int,而 param 將要對這幾種類型的變量建立引用,ParamType 就推導出了上述的結果。
其中很重要的一點:
當傳遞一個 const 對象到一個引用參數(parameter)時,調用者希望這個對象能保持 const 特性,即不變性。
模板類型推導遵從這一要點。故傳遞 const 對象到模板參數 T& 是安全的,不會丟失 const 屬性。
並且以上規則對於右值引用也是成立的。
而將上例中的 ParamType 改為 const T& 時,上例三次調用全部將 ParamType 推導為 const int&,T 則每次都為 int。因為 ParamType 的形式中帶有了 const,匹配後 T 就不需要帶有 const 了。
而對於 ParamType 是指針的情形,推導過程也是同樣的。只是去除了“忽略引用部分”這一步,只是對指針類型進行模式匹配。
模板函數的參數是universal引用的時候,比如“像是”右值(rvalue)引用,即 T&& 這樣的類型,其中 T 的模板類型參數。
我想,所謂universal引用,可以先參考“引用疊加效果”表:
& & -> &
& && -> &
&& & -> &
&& && -> &&我想這可能念做:&引用的&引用是&引用,&引用的&&引用是&引用,&&引用的&引用是&引用……
我的理解是,其中任一種引用的&&引用都是原型,所以叫做universal引用吧。由於是疊加在未確定的模板類型 T 上的,所以寫法雖然一樣,但並不是右值引用,因為右值引用是作用於明確類型上的。
參考資料 http://stackoverflow.com/questions/20364297/why-universal-references-have-the-same-syntax-as-rvalue-references
對於universal引用,類型推導規則為:
規則1可以對照引用疊加表,expr 的類型就是 -> 號右邊的,如果它是左值即&,能通過universal引用變成這樣狀態的也只有左值&。即使 ParamType 被聲明為和右值引用類似的形式,ParamType 本身也被推導為左值引用。
我認為這一推導正是由於待定的 ParamType 並不能表示一個右值引用類型,而只能作為一種“帶有未知量 T”的類型運算表達式。比如 T 若是 int&&,則 T& 就是 int&。
書中對於情形2的例子為:
template<typename T> void f(T&& param); // param is now a universal reference
int x = 27; const int cx = x; const int& rx = x;
f(x); // x -> lvalue, T -> int&, ParamType -> int& f(cx); // cx -> lvalue, T -> const int&, ParamType -> const int& f(rx); // rx -> lvalue, T -> const int&, ParamType -> const int& f(27); // 27 -> rvalue, T -> int, ParamType -> int&&
第4個調用即為退回情形1規則的情況。expr 是一個右值,則進行模式匹配後被綁定到 int&&,其中 T 為 int。
就像這樣,按值傳遞/按拷貝傳遞(pass-by-value):
template<typename T> void f(T param);
那麼 param 總是對實參(argument)進行拷貝。此情形有規則:
int x = 27; const int cx = x; const int& rx = x;
f(x); // T and ParamTypes -> int f(cx); // T and ParamTypes -> int f(rx); // T and ParamTypes -> int
因為 param 總是 expr 的拷貝,所以無論怎樣都不會影響 expr,所以 expr 的 const、volatile 這些特性,都和 param 無關了。
這也正符合上面所說的,調用者希望傳入的對象原本具有的特性(如 const)不受影響,程序的實現要遵從這一希望。
原書在這裡舉了一個 const char * const 按拷貝傳遞(按值傳遞——相對於按引用傳遞)的例子,不細表。
C/C++都有這樣一個特性,那就是數組的退化(decay):
const char str[] = "hello"; // const char[6] const char *p = str; // 數組退化為指針
很明顯 str 和 p 的類型是不同的。而且對於C中的語法,是可以將函數的參數聲明為數組的形式的,但是以下兩者卻是相同的:
void func(char str[]); void func(char *str);
這是因為數組形式的形參(parameter),會被當作指針形式的形參處理。
因此對於按值傳遞的模板參數 T 來說,實參為數組 char[] 時,T 被推導為 char *。(可以認為數組的退化先發生。)
但模板參數為引用的時候,是能“真正”引用到傳入的數組的(即不發生數組退化):
template<typename T> void f(T& param); f(str); // T -> const char[6], ParamType -> const char (&)[6]
一個例子,通過模板在編譯期獲取數組大小(代碼中暫時無關的部分被去掉了):
template<typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) { return N; }
除了數組之外,函數也會退回為指針。但同時,同樣能通過模板提供引用類型參數來避免退化:
void someFunc(int, double); // someFunc -> void(int, double)
template<typename T> void f1(T param);
template<typename T> void f2(T& param);
f1(someFunc); // ParamType -> void (*)(int, double) f2(someFunc); // ParamType -> void (&)(int, double)
數組和函數的退化都是針對其標識符。