在C++程式設計語言中,decltype作為運算子,用於取得表達式的資料類型。C++11標準引入decltype,主要是為泛型編程而設計,以解決泛型編程中有些類型由模板參數決定而難以(甚至不可能)表示的問題。
從語意上說,decltype的設計適合於通用庫編寫者或編程新手。總體上說,對於變數或函式參數作為表達式,由decltype推導出的類型與原始碼中的定義可精確匹配。而正如sizeof運算子一樣,decltype不對運算元求值。
簡要理解
雖然C++規範或大多數的文件及書籍都將decltype定義為一個運算子,但是針對初學者而言,應將此運算子理解為一種特殊的類型聲明,而不是像一些強型別語言那樣將此運算子理解為一個帶返回值的函式。為說明此問題,舉例如下:
對於其它語言的開發者,可能寫出如下的代碼:
#include <iostream>
using std::cout;
int main()
{
int n = 12;
cout << decltype(n);//實際上decltype與js或C#的typeof完全不一樣
getchar();
return 0;
}
相反,可以將decltype視為一種特殊的類型聲明方式,C++語言裡面類似的功能為自動變數。
舉例:設開發人員無法確定一個表達式exp()的值類型,然而需要聲明一個與之類型匹配的變數,則可以如下聲明:
decltype(exp()) x;
設計構想
隨著C++引入模板,以及由標準模板庫引領的泛型編程逐漸興起,實現一個能取得表達式類型的機制的需求便由此出現,而這一機制常稱為typeof。在泛型編程中,若類型由函式參數決定,則獲知之常非易事[1][2],在需要取得函式模板實例化的返回類型時尤然[1]。
為此,許多編譯器廠商都基於程式語言現有的功能,自行實現了這類運算子,其實現如typeof,以及一些功能有限,但更易移植的實現,以滿足這一需求[3]。早在C++還未完全標準化的1997年,布萊恩·帕克(Brian Parker)就基於sizeof運算子,提出了一種可移植的解決方案[3]。對此,比爾·吉本斯(Bill Gibbons)則提出,這一方案仍有諸多限制,而且通常來說,直接引入typeof機制效果都更好[3]。2000年10月,安德烈·亞歷山德雷斯庫在IT技術雜誌《Dr. Dobb's Journal》上評論道:「(若)有typeof(運算子),撰寫和理解模板代碼就會便易許多。[4]」他也提到「typeof和sizeof(運算子)有相同的後端,(這是)因為sizeof無論如何必須去計算類型。[4]」安德魯·克尼格與芭芭拉·E·摩(Barbara E. Moo)也談到內建於程式語言中的typeof功能非常有用,但也提醒道「使用時常會引入一些難以發覺的程式錯誤,且尚有無法解決的問題(即並非萬用)。[5]」並提出可以利用類型轉換(如使用標準模板庫所提供的typedef),更有效、更通用地實現這一功能[5]。但是,史蒂夫·丹斯特(Steve Dewhurst)則稱如此轉換「在設計與發布上花費巨大」,而且「採用直接提取表達式類型的方法更簡單[6]」(大意)。2011年間,在一片關於C++0x的文章中,克尼格和摩預言道:「decltype將會廣泛用於為每日的程式編寫提供便利。[7]」
2002年間, 比雅尼·史特勞斯特魯普提議擴充C++程式語言,為之引入查詢表達式類型,以及不必指明類型便可初始化對象的機制[1]。史特勞斯特魯普注意到,在GCC與EDG編譯器中,typeof所提供的「參照丟棄」(reference-dropping)語意可能存在問題[1];另一方面,若使用基於表達式左值性、返回一個參照類型的運算子實現之,又難以理解。於是,在呈交給C++標準委員會的初始提案中,便將兩種實現方法雜糅起來:只有當表達式的聲明類型包含一個參照時,運算子才會返回一個參照類型。為強調推導出的類型能確實反映表達式的聲明類型,提案中提議將此運算子命名為decltype[1]。提案還提及了decltype的一項主要設計初衷,也即讓編寫完美的轉發函式成為可能[8]。在編程時,程式設計師有時需要編寫一個泛型轉發函式,使之不論以何種類型實例化,都能返回同於包裝函式的類型,而若無decltype運算子,就幾乎不可能做到這一點[8]。decltype的樣例代碼如下所示,其中利用了C++11標準中的「返回類型後置」(trailing-return-type)語法[8]。
int& foo(int& i);
float foo(float& f);
template <class T> auto transparent_forwarder(T& t) −> decltype(foo(t)) {
return foo(t);
}
decltype便是本段代碼的核心部分,用於儲存「包裝函式是否返回一個參照類型」這一資訊 [9]。
語意
類似於sizeof運算子,decltype不對其運算元求值[10]。粗略來說,decltype(e)返回類型前,進行了如下推導[1]:
- 若表達式e為一個無括號的變數、函式參數、類別成員訪問,那麼返回類型即為該變數或參數或類別成員在源程式中的「聲明類型」;
- 否則的話,根據表達式的值分類(value categories),設T為e的類型:
這些語意是為滿足通用庫編寫者的需求而設計,但由於decltype的返回類型總與對象(或函式)的定義類型相匹配,這對編程新手來說也更為直觀[1]。更正式地說,規則1適用於不帶括號的識別碼表達式(id-expression)與類別成員訪問表達式[11][12]。範例如下[11][a]:
const int&& foo();
const int bar();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1; // 类型为const int&&
decltype(bar()) x2; // 类型为int
decltype(i) x3; // 类型为int
decltype(a->x) x4; // 类型为double
decltype((a->x)) x5; // 类型为const double&
由上可見,最後兩個對decltype的呼叫,返回結果有所不同。這是因為,帶括號的表達式(a->x)既非「識別碼表達式」,亦非類訪問表達式,因而未指向一個命名對象[13],而是一個左值,於是推導類型便為「指向表達式類型的參照」,亦即const double&[10]。
在2008年12月,雅克·雅爾維(Jaakko Järvi)向標準委員會指出一個問題:在C++中,「帶限定識別碼」(qualified-id)無法由decltype作成[14],而這正與「decltype(e)可作『類型定義名』(typedef-name)看待」的設計初衷不一致[15]。在評論標準委員會為C++0x(C++11前名)制定的正式草案時,日本ISO會員成員提到,「一個定義域運算子(::)不適用於decltype,但本應適用才對。(若能解決這一問題,則)這在需要從實例中取得成員類型(巢狀類型)很有用,如下所示[16]」:
vector<int> v;
decltype(v)::value_type i = 0; // int i = 0;
這一問題,以及其他相似問題(關於decltype無法在衍生類聲明和解構函式呼叫中使用),都交由大衛·范德沃德(David Vandevoorde)處理,並在2010年3月投票納入工作日程表[17][18]。解決辦法是:如果表達式是純使用者定義類型(plain user defined type),即不是參照、指標、函式等,則decltype表達式返回類名。這意味著可以訪問巢狀類型。甚至可以用decltype(expr)
作為衍生時的基本類型。
可用性
decltype包含於當前的C++標準C++11中[11],並由許多編譯器以擴充的形式提供:微軟在Visual C++ 2010編譯器中提供了decltype運算子,基本實現了標準委員會提案中所描述的語意,並且在代管代碼或原生代碼中都可使用[9]。據其文件稱,這一實現「主要對編寫模板庫的開發者有用。[9]」從2008年3月5日發布的4.3版開始[19],GCC C++編譯器也加入了decltype運算子[20]。這一運算子也已納入了Codegear的C++ Builder 2009[21]、Intel C++編譯器[22]與Clang[23]。