概念 or 忠告
- The distinction between arguments and parameters is important, because parameters are lvalues, but the arguments with which they are initialized may be rvalues or lvalues
形參(parameter,函數頭聲明的參數)和實參(argument,或傳參)的差異在C++11中變得格外重要,因為引入了左值和右值,形參是左值,但實參既可以是左值,又可以是右值。參數在傳遞時,腦海中通常也會模擬一個等式,左邊是形參,而右邊是實參。 - I define a function’s signature to be the part of its declaration that specifies parameter and return types. Function and parameter names are not part of the signature.
約定:函數的簽名僅包括返回值、參數列表 - Sometimes a Standard says that the result of an operation is undefined behavior. That means that runtime behavior is unpredictable
什么是未定義行為:即它的運行時行為是不可預測的
Term I Use | Language Version I Mean |
---|---|
C++ | All |
C++98 | C++98 or C++03 |
C++11 | C++11 and C++14 |
C++14 | C++14 |
約定:當你說不同C++的代號時,分別對應著哪個版本
特性:
- C++以性能著稱
- C++98缺乏并行性(concurrency)
- C++11支持lambda表達式,C++14支持通用的函數返回推斷
- C++11最為廣泛的影響即
move
語義
- as a modern C++ developer, you’d naturally prefer a std::array to a built-in array
作為一個高級C++程序員,更應該選擇使用std::array
類型推斷
- 好處:無需重復的拼寫顯而易見的類型
- 壞處:讓代碼更難懂,編譯器行為并不是那么直觀
如果不能理解類型推斷操作,在高級C++中高效編程幾乎不可能實現,因為類型推斷無處不在。
#1 理解模板類型推斷
假設有以下模板定義
template<class T>
void f(ParamType param);
以及如下調用
f(expr);
編譯器會通過expr
來推斷T
和ParamType
的類型,除了expr
之外,ParamType
的形式也很重要,ParamType
的形式分3種情況
-
ParamType
是一個指針或引用類型,但不是一個universal reference
如果expr
是一個引用類型,則首先去引用,再根據ParamType
的類型,來推斷出T
的類型,例如
template<class T>
void f(T ¶m);
// ...
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 &
可以看到,第2和第3個參數為T
賦予了const
屬性,這對調用者來說是非常重要的的,可以利用此保證外部引用參數不被修改,即傳const引用的對象
給參數為T&
類型的模板是安全的
如果將T &
改為const T &
,則實參的const
屬性也會去掉
template<class T>
void f(const T ¶m);
// ...
int x = 27;
const int cx = x;
const int &rx = x;
f(x); // T為int,ParamType為const int &
f(cx);// T為int,ParamType為const int &
f(rx);// T為int,ParamType為const int &
你也可以通過下載github上的代碼來驗證運行結果。
如果把引用&
換成指針*
,以上規則仍然生效。
1.1 傳入數組
C++一般不可以在函數參數中定義數組類型,取而代之的是將數組的第一個元素的地址作為參數,但模板參數的引用形式將數組參數成為可能,例如
template<class T>
void f(T ¶m);
// ...
const char name[] = "J. P. Briggs";
f(name); // T為const char [13],ParamType為const char (&)[13]
在此場景下,編譯器可以推斷數組的長度,我們可以重新定義模板,以在編譯時獲得數組長度
// 在編譯時返回一個數組長度,這里只關注數組長度,所以忽略了數組名
// constexpr關鍵字使結果在編譯期可用
template<class T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{
reutrn N;
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
std::array<int, arraySize(keyVals)> mappedVals; // 數組長度為7
-
ParamType
是universal reference(參數聲明類型為T&&)
a) 當參數為左值時,T
被推斷為左值引用,這也是唯一的T
被推斷為左值引用的情況(其他情況下,引用會被去掉)。
b) 即便參數為右值引用,當參數為左值時,ParamType
也仍然為左值引用
c) 在參數為右值時,規則和第1條規則保持一致
template<class T>
void f(T &¶m);
// ...
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 &
f(27); // 因為27為右值T為int,ParamType為int &&
-
ParamType
既不是引用,也不是指針類型——pass-by-value
a) 如果expr
是一個引用,忽略引用部分
b) 如果expr
有const
或volatile
屬性,也忽略這一部分
template<class 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為int,ParamType為int
f(rx);// T為int,ParamType為int
考慮更復雜的情況,如果實參是一個const
類型的指向常量字符串的指針,那么當其進行copy-by-value
傳參時,會發生什么情況
const char * const ptr = "Fun with pointers";
f(ptr); // T為const char *,ParamType為const char *
意為字符串的const
屬性被保留,但指針的const
(const
在*
號右邊)屬性被移除。記住:只有在copy-by-value條件下,const性質才會被移除
如果給copy-by-value
的模板傳入一個數組,會被解析成什么樣子?和ParamType
為引用類型不一樣的是,參數仍然會被推斷為指針類型
const char name[] = "J. P. Briggs";
f(name); // ParamType被推斷為const char *
#2 理解auto
類型推斷
auto type deduction is template type deduction.
auto類型推斷就是template類型推斷
auto類型推斷和template類型推斷之間有一個映射關系,以下表格第一列和第二列等價
template類型推斷 | auto類型推斷 |
---|---|
template<class T> void func_for_x(T param); func_for_x(x);
|
auto x = 27; |
template<class T> void func_for_cx(const T param); func_for_cx(cx);
|
const auto cx = x; |
template<class T> void func_for_rx(const T ¶m); func_for_rx(rx) ; |
const auto &rx = x; |
上表可以看出,auto
關鍵字對應模板中的類型T
,而等式左邊除變量以外的部分對應著模板參數中的ParamType
;auto
類型推斷和模板類型推斷一樣,也滿足3種條件
- 參數類型為指針或引用,但不是universal reference
auto x = 7; // x的類型為int
auto &rx = x; // rx的類型為int &
- 參數類型為universal reference
auto x = 7;
const int cx = 7;
auto &&uref1 = x; // uref1的類型為int &
auto &&uref2 = cx; // uref2的類型為const int &
auto &&uref3 = 27; // uref3的類型為int &&
- 參數類型既為pass-by-value
auto x = 7;
等式右邊如果是一個數組,auto
的行為也和模板類型推斷一樣
const char name[] = "J. P. Briggs";
auto pname = name; // pname的類型為const char *
auto &rname = name; // rname的類型我const char (&)[13]
等式右邊如果是大括號初始化列表,左邊的推斷類型為std::initializer_list
// x3和x4的類型為std::initializer_list<int>
auto x3 = {27};
auto x4{27};
這里實際上有兩層類型推斷,第一層為判定x3
的類型為std::initializer_list<T>
,第二層為根據27
判斷T
的類型為int
,所以如果大括號中的數據類型不一致,編譯器將會報錯,例如
auto x5 = {1, 2.0}; // cannot deduce actual type for variable 'x5' with type 'auto' from initializer list
auto x6 = {1,2}; // ok
上述推斷也是auto
和template
類型推斷的唯一不同之處,如果你這樣向一個模板類型參數傳參,編譯器會報錯
template<class T>
void f(T arg);
f({1,2}); // wrong. candidate template ignored: couldn't infer template argument 'T'
因為編譯器對待template
類型推斷,不會像auto
一樣,進行兩次推斷,所以要想順利編譯通過,要對模板函數修改如下:
template<class T>
void f(std::initializer_list<T> arg);
auto
在C++11
中的規則到這里就結束了,但在C++14
中并沒有,C++14
允許函數的返回類型或lambda
表達式參數使用auto
推斷,但這里的規則卻是template
的推斷方式,舉兩個例子就清楚了
// case 1
auto initial() {
return {1,2}; // error: can't deduce type for {1,2}
}
// case 2
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; };
resetV({1,2,3}); // error: can't deduce type for {1,2,3}
很奇怪是不是,這個規則沒有特別的原因——Rule is the rule!
#3 理解decltype
decltype - 輸入一個名字或表達式,decltype
會告訴你該輸入的類型。在C++11中,decltype
常被用在模板函數的返回類型中,這個返回類型通常由模板的參數決定,例如,我們想讓函數具備和operator[]
一樣,返回容器內具體元素的引用,可以這樣聲明模板:
template<typename Container, typename Index>
auto authAndAccess(Container &c, Index i)
-> decltype(c[i])
{
return c[i];
}
以上代碼,auto
不會起到任何作用,函數的返回類型被C++11的尾部返回類型(trailing return type)取代,這種方式的好處是,聲明的返回類型可以依賴于函數的參數,如果像傳統方式一樣,把它們寫在函數名之前,會報「c和i沒有被聲明」的錯。
但值得注意的是,C++14不需要這樣,僅靠auto
即可實現返回類型推斷,但根據#1
和#2
的規則,返回的值會失去引用屬性,則下面的用法會報錯,因為右值不可以被賦值
std::deque<int> d;
...
authAndAccess(d, 5) = 10;
要讓authAndAccess
函數和[]operator()
的返回類型一致,就需要借助decltype
,模板聲明要修改如下
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container &c, Index i);
除此之外,decltype
還被用作等式賦值操作中,例如
const int &i = 10;
auto x = i; // x 為int類型
decltype(auto) dx = i; // dx為const int &類型
但如果還想讓authAndAccess
函數接受右值,滿足對臨時容器元素的復制需求,如
std::deque<std::string> makeStringDeque(); // 工廠函數
auto s = authAndAccess(makeStringDeque(), 5);
又該如何修改authAndAccess
呢?這里就要universal reference
派上用場了。
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container &&c, Index i)
{
return std::forward<Container>(c)[i];
}
而如果在C++11的環境下,以上代碼需要手動聲明返回類型
template<typename Container, typename Index>
auto
authAndAccess(Container &&c, Index i)
-> decltype(std::forward<Container>(c)[i])
...
在使用decltype(auto)
時,還有一點需要注意,當需要推斷的參數是一個表達式,而不僅僅是一個名字時,decltype
會將其推斷為具體類型的引用(T &),尤其值得注意的是,變量x
和(x)
具有不同的性質,前者是一個變量名,而后者被解釋為表達式,所以假設x
是int
類型的情況下,decltype(x)
被推斷為int
,而decltype((x))
被推斷為int &
,例如
int ix = 10;
decltype(auto) dix = ix;
dix = 11;
cout << "ix:" << ix << endl; // ix: 10
decltype(auto) deix = (ix);
deix = 11;
cout << "ix:" << ix << endl; // ix: 11
于是當decltype(auto)
作為函數的返回類型時,return
語句需要小心對待,尤其是return
一個局部變量時,千萬不能將其用圓括號括起來,否則你會返回一個臨時對象的引用,你的程序的行為就是未定義的。
#4 了解如何查看推斷類型
比較靠譜的獲取對象類型的方法是使用Boost.TypeIndex庫(<boost/type_index.hpp>),使用其中的boost::typeindex::type_id_with_cvr<T>()
模板,該模板接受的參數即為你想要診斷的變量或表達式的類型,cvr
代表const
、volatile
以及reference
,表示這三種類型不會被省略。
template<typename T>
void f(const T ¶m)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
cout << "T = " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "param = " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
本文是對《effective Modern C++》一書的第一章節的筆記,想深入學習該內容的同學還請以原書為準。
參考:
《effective modern C++》by Scott Meyes