C++ 11中的一些典型概念與分析
相比傳統的C++98與C++03, C++11中新提出了很多新的概念,本文根據C++佈道師Scott Meyers 在Youtube上的培訓視訊展開,介紹C++11中的一些典型概念,並進行分析。
1. 左值(lvalue)與右值(rvalue)
C++11之前已經有左值與右值的概念,但由於其只是簡單的概念,並無太多應用,關注的不多;C++11中則貫穿了左值和右值的相關應用,比如型別推斷等。判斷左右值的依據非常簡單: 如果一個物件可以取地址,就是左值,反之就是右值。 基於左值和右值,出現了左值引用和右值引用,它們廣泛的被應用到型別推斷中。 左值引用就是對左值的引用,右值引用就是對右值的引用。
int a = 3; int& b = a; //左值引用 int&& c = 3; //右值引用 int&& d = int(3); //右值引用
2. 型別推斷(type deduction)
型別推斷在C++98中就有,C++11中將其擴大化,它們的關係如下圖所示: C++11在C++98的基礎上引入了廣義引用T&&(Universal Reference),decltype,並引入了auto object; 另外,擴充套件C++98中T&/T*等到lamada按值或按引用捕獲,擴充套件T到lamada隱式返回等。 型別推斷的一般問題如下:
template <typename T>
void f(ParamType param)
就是根據函式f(expr)呼叫中expr的型別來推斷ParamType以及T的型別;通常有以下幾種情形:
2.1 ParamType型別是廣義引用(Universal Reference)的情形
其具體的表現形式為:
template<typename T>
void f(T&& param); // param is now a universal reference
廣義引用和右值引用不同,右值引用只能繫結到右值,廣義引用可以繫結到左值或者右值,但當它們繫結到左值或右值時,呼叫f(expr)進行型別推導的規則不同:
- 如果expr是左值,那麼T和ParamType將會被推導為左值引用
- 如果expr是右值,那麼ParamType將會被推導為右值引用, T根據實際型別決定 例如:
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&
廣義引用和右值引用均有相同的&&符號,但它們是有區別的,廣義引用只在型別推導中產生並且要求T的所有型別均是推匯出來的,否則其是右值引用,例如:
void f(Widget&& param); // rvalue reference,param's type is fixed and deduction is not happened
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // lvalue reference(var1's type is rvalue ref,
// but var1 itself is a lvalue since it has name and can take address
template<typename T>
void f(std::vector<T>&& param); // rvalue reference
template<typename T>
void f(T&& param); // not rvalue reference
區別廣義引用和右值引用,只需記住如下幾點:
- 對於待推導的型別,如果一個函式模板的模板引數型別是T&&或者一個物件宣告用的是auto &&, 那麼模板引數或者物件就是廣義引用;
- 如果沒有型別推導或者型別推導的引數型別不是T&&或者auto &&(例如引數型別是const T&&或者宣告物件用的是const auto&&),那麼模板引數或者物件就是右值引用;
- 廣義引用等價與右值引用如果引數型別是右值;等價於左值引用如果引數是左值
2.2 ParamType型別是引用或指標, 但不是廣義引用的情形
其具體表現形式為:
template<typename T>
void f(T& param); // param is a reference
呼叫f(expr)時進行型別推導的規則如下:
- 如果expr的型別是引用,那麼忽略引用部分進行型別推導
- 然後用模式匹配的方式分析expr的型別和ParamType來決定T的型別
例如:
int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int
f(x); // T is int, param's type is int&
f(cx); // T is const int, param's type is const int&
f(rx); // T is const int, param's type is const int&;rx’s type is a reference, T is deduced to
be a non-reference, because rx’s reference-ness is ignored during type deduction
如果將模板引數中的T&變為const T&, 情況也類似,對於:
template<typename T>
void f(const T& param); // param is now a ref-to-const
呼叫f(expr)時進行型別推導時,規則和上面相同,只是在進行模式匹配推導T時模式稍有不同(多了const), 例如:
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
對於引數型別為T*的情形,型別推導規則相同:
template<typename T>
void f(T* param); // param is now a pointer
int x = 27;
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param's type is int*
f(px); // T is const int, param's type is const int*
2.3 ParamType不是引用或者指標的情形
函式模板如果按照下面pass-by-value的方式進行:
template<typename T>
void f(T param); // param is now passed by value
那麼則意味著引數param將會獲得外面傳入引數的一個副本,也就是說不管f(expr)呼叫時其型別是什麼,const,volatile,引用,param的變化將不會對外部傳入引數有任何的影響,那麼按照這種pass-by-value的方式進行的型別推導規則也就非常清晰了:
- 如果expr的型別含有引用,忽略引用部分進行推導;
- 如果忽略引用部分後還有const, &, volatile 等,忽略這些部分進行型別推導
例如:
int x = 27;
const int cx = x;
const int& rx = x;
const int* px = &x;
const int* pcx const= &x;
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int
f(px); // T's and param's types are both const int *
f(pcx); // T's and param's types are both const int *
這裡要注意,對於指標的情況,如果是指標常量,因為是按值傳遞,指標的常量屬性將會被忽略;如果是常量指標,則這個屬性會保留;例如上面的px, 不能通過它改變它指向的量;pcx的型別是const int * const, 指標的常量屬性在按值傳遞時會被忽略。
對於陣列名,陣列型別在按值傳遞時會退化為指標:
const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char* ptrToName = name; // array decays to pointer const char*
f(name); // T's and param's types are both const char*
但是,陣列型別在按引用傳遞時會保持陣列型別:
template<typename T>
void f(T& param); // template with by-reference parameter
對於上面的按引用傳遞:
f(name); // T's type is const char[13], param's type is const char(&)[13]
按引用傳遞不會丟失陣列容量的這個特徵可以應用到模板中,使得可以在編譯期獲得陣列容量,:
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}