《Effective Modern C++》筆記之【類型推斷】

概念 or 忠告

  1. 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中變得格外重要,因為引入了左值和右值,形參是左值,但實參既可以是左值,又可以是右值。參數在傳遞時,腦海中通常也會模擬一個等式,左邊是形參,而右邊是實參。
  2. 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.
    約定:函數的簽名僅包括返回值、參數列表
  3. 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語義
  1. 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來推斷TParamType的類型,除了expr之外,ParamType的形式也很重要,ParamType的形式分3種情況

  1. ParamType是一個指針或引用類型,但不是一個universal reference
    如果expr是一個引用類型,則首先去引用,再根據ParamType的類型,來推斷出T的類型,例如
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為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 &param);
// ...
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 &param);
// ...
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
  1. ParamType是universal reference(參數聲明類型為T&&)
    a) 當參數為左值時,T被推斷為左值引用,這也是唯一的T被推斷為左值引用的情況(其他情況下,引用會被去掉)。
    b) 即便參數為右值引用,當參數為左值時,ParamType也仍然為左值引用
    c) 在參數為右值時,規則和第1條規則保持一致
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為const int &,ParamType為const int &
f(rx);// T為const int &,ParamType為const int &
f(27); // 因為27為右值T為int,ParamType為int &&
  1. ParamType既不是引用,也不是指針類型——pass-by-value
    a) 如果expr是一個引用,忽略引用部分
    b) 如果exprconstvolatile屬性,也忽略這一部分
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屬性被保留,但指針的constconst*號右邊)屬性被移除。記住:只有在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 &param);

func_for_rx(rx);
const auto &rx = x;

上表可以看出,auto關鍵字對應模板中的類型T,而等式左邊除變量以外的部分對應著模板參數中的ParamTypeauto類型推斷和模板類型推斷一樣,也滿足3種條件

  1. 參數類型為指針或引用,但不是universal reference
auto x = 7; // x的類型為int
auto &rx = x; // rx的類型為int &
  1. 參數類型為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 &&
  1. 參數類型既為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

上述推斷也是autotemplate類型推斷的唯一不同之處,如果你這樣向一個模板類型參數傳參,編譯器會報錯

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);

autoC++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)具有不同的性質,前者是一個變量名,而后者被解釋為表達式,所以假設xint類型的情況下,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代表constvolatile以及reference,表示這三種類型不會被省略。

template<typename T>
void f(const T &param)
{
  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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容