C++ template 為什麼不能推導返回值類型?
例如:
希望能有透徹一點的解釋。如果有什麼解決方案(如C++11和boost的一些高級用法),也希望能一併回答出來。謝謝。
template&
T value()
{
// for expample
T* ptr = static_cast&(_ptr);
return *ptr;
}
討論之前先說一下,結合上面的補充說明來看,這個問法其實有一點點瑕疵,這也導致了大家在回答時的一些誤會。題主這麼問,是因為 C++ 確實提供了函數模板的參數類型推導(通過調用方提供的信息,自動推斷並填充到模板參數,從而避免用戶手動指明模板參數)。
這樣看,這個問題的正確提法應該是這樣的:
「C++ template 為什麼不能像典型的參數類型推導那樣,通過判斷調用方提供的返回值類型,將其自動填充到模板參數,從而避免用戶手動指明模板參數?」
-------------------------------------
看了一下現有的回答,發現大家在 「類型推導」 (Type Deduction) 上,其實沒有在說同一件事,所以我覺得有必要先澄清一下這個概念。
@Milo Yip 同學在 @黃柏炎 同學答案的評論中提到:「並不是從調用方推導。C++14可以靠return的類型推導。」
那麼 "C++14 返回值類型推導" 和題主問到的 "函數模板參數類型推導" 是一碼事嗎?答案是否定的。我這裡詳細說明一下。
題主的例子中,所謂 "推導" 指的是編譯器在某些情況下,可以根據調用方提供的信息來補全用戶未提供的模板參數,是模板實例化 (template instantiation) 的一個步驟,發生的時機是在函數模版的調用時(invoke time of function template)。也就是說,當需要的時候,每次模版函數的調用,均會 (根據調用方提供的信息) 觸發一次潛在的模板參數類型推導。
而 @空明流轉 , @vczh@Milo Yip 等同學在答案或評論中提到的 "C++14 返回值類型推導",則分為普通函數和模板函數兩種情況:
1. 當為普通函數時,返回值類型推導是函數體的一部分,發生在函數定義 (function definition) 時。舉個栗子,形如 auto foo(int typedArg) { return typedArg; } 的函數,在定義時已可完全確認返回值類型為 int 了。
2. 當為模板函數時,返回值類型推導仍為函數體的一部分,但需根據其"是否依賴模板參數類型"來決定發生於定義時還是實例化時。當返回值類型依賴模板參數類型時,情形正如 @vczh 同學舉的例子;當返回值類型不依賴模板參數類型時,則退化為 1. 中的普通函數調用情況。請注意,對於 2. 中提到的 @vczh 同學舉的例子,調用方仍需提供模板參數類型,無論是藉助編譯期推導還是手工填充。
總得來說,"C++14 返回值類型推導" 是一個正向過程,只是語法上的一種簡化 (syntax simplification),而語義上與原來的函數完全一致。而題主問到的 "函數模板參數類型推導" 是一個反向過程,在語義上,返回值的類型"接受推導"和"不接受推導"會導致截然不同的函數特化和調用。
-------------------------------------
好了,辨別清楚了概念,現在我們來正面回答這個題目:
為了儘可能與 C 保持語法和語義上的兼容性,在 C++ 中,對於函數的調用方而言,返回值總是可以忽略的。
也就是說,對於給定的函數int foo()
{
return 0;
}
foo(); // 忽略返回值
對於模版函數而言,如果依賴返回值做模板的類型推導,就會出現由於調用信息不全導致的二義性。
還是剛才這個例子,我們改為對應的函數模版,
template &
T foo()
{
return T(0);
}
假如我們允許藉助返回值來推導(如下所示)
int a = foo(); // 特化為 foo&
double b = foo(); // 特化為 foo&
那麼當調用方像之前的例子那樣調的時候,編譯器就沒辦法處理了:
foo(); // 報錯,因為缺乏足夠信息做模板實例化
正如 @黃柏炎 同學所提到的,函數重載時,情況雖略有不同,導致了語義上的處理稍有不同,但最後也產生了類似的效果。
那麼總結一下,一句話結論——「為了與C保持兼容,返回值並非是調用函數時的必要條件,因此函數模版類型推導和函數重載都不能且不應依賴返回值。」
-------------------------------------
如果你只想了解這個問題本身,那麼到剛才的一句話結論就可以結束了。然而,對模板而言,函數返回值與函數簽名之間的關係實際上要更複雜一些。咱們剛剛也提到,函數模版類型推導和函數重載,看起來在語法上具有某種形式上的一致性,兩者在語義上是有所不同的。如果您感興趣,可以接著往下讀,我們刨根問底一下,看看返回值究竟在函數簽名中扮演了什麼角色,順便弄清楚兩者究竟有何不同。
-------------------------------------
先解釋一下函數類型 (Function Type) 和函數簽名 (Function Signature) 吧。
在 C++ 中,函數類型 (Function Type) 與函數簽名 (Function Signature) 是兩個完全不同的概念。在我的理解中,前者主要是給程序員用的,通常用來定義函數指針 (形如 void(*)() ) 和函數對象 (形如 std::function&
標準規定 (見 1.3.11 對函數簽名的說明和 14.5.5.1 對模版函數特化時簽名的補充說明):
- 對於普通函數(非模版函數),函數的簽名包括未修飾的函數名 (function name) ,參數類型列表 (parameter type list)和所在類或命名空間名 (class and namespace name)
- 對於類成員函數,函數的簽名除了 1 中提到的以外,還包括 cv 修飾符 (const qualifier and volatile qualifier) 和引用修飾符 (ref qualifier)
- 對於函數模板,函數的簽名除了 1 和 2 中提到的以外,還包括返回值類型和模板參數列表
- 對於函數模板的特化 (function template specilization),函數的簽名除了 1, 2 和 3 中提到的以外,還包括為該特化所匹配的所有模板參數(無論是顯式地指定還是通過模板推導隱式地得出)
-------------------------------------
下面,我們先來挨個看看如何用標準來解釋上面的幾種行為,再來看看標準為什麼對函數的簽名做這樣的規定。
Q1: 普通的函數重載時發生了什麼?
A1. 函數的重載決議機制,依賴了函數簽名的獨特性。標準的 1 和 2 中,並沒有提到返回值類型,因此我們可以認為,僅有返回值不同的函數重載是無效的,因為根據標準,它們簽名是完全一致的。
例如下面兩個函數:void bar() {}
int bar() { return 0; }
在函數定義(不用等到調用)的時候就無法通過編譯,因為同一個編譯單元 (translation unit) 中出現了兩個簽名一致的函數。
Q2: 函數模板實例化時發生了什麼?
A2. 根據 3 和 4 可以知道,通過在簽名中包含返回值類型和模板參數列表,一個函數模板及其若干特化得到了某種程度上的強類型保證,當所提到的類型不一致時,編譯器有機會報出對應的錯誤。Q3: 函數模板實例化時,如果觸發了類型推導,發生了什麼?
A3. 當類型信息提供不完全,需要編譯器推導時,從 3 可以知道,由於簽名中已經包含了所有必要的信息,編譯器有能力藉助簽名本身得知必要的類型信息並進行補全。Q4: 函數模板實例化時,跟返回值相關的行為是什麼?A4. 返回值是簽名的一部分,這個事實導致了下面的定義方式成為可能:template&
template&
請注意,跟 Q1 中 "定義時就無法通過編譯" 不同的是,這兩個同名同參的函數的定義是可以通過編譯的,因為根據 3 可以知道,返回值是簽名的一部分,這兩個函數的簽名是不同的。但實際使用時,根據我們之前的「一句話結論」中提到的,(為了與C保持兼容,返回值並非是調用函數時的充分必要條件),當真正的調用發生時,編譯器有可能缺乏足夠的信息去了解返回值的類型,也就不知道該把函數調用決議到哪一個函數定義上去。這個錯誤理論上來講可以是一個鏈接錯誤,但由於在函數定義的編譯階段已經可以得到了兩個不同的函數,那麼實際結果是在調用方的編譯階段就可以報出錯誤了。
Q5: 模板特化和重載決議同時觸發時,會發生什麼?
A5. 喜歡刨根究底的同學肯定會產生這個疑問,這裡我們舉兩個例子:例子1,這個例子中,我們不僅期望函數模板會自動推導模板參數,而且期望編譯器能夠選擇正確的重載版本去調用
template&
int f(T)
{
return 1;
}
template&
int f(T*)
{
return 2;
}
int main()
{
std::cout &<&< f(0) &<&< std::endl;
std::cout &<&< f((int*)0) &<&< std::endl;
}
例子2,這個例子中,我們重載了模版函數和非模板函數,和例子1一樣,我們不僅期望 (在必要時) 函數模板會自動推導模板參數,而且期望 (在必要時) 能夠選擇正確的重載版本去調用:
#include &
#include &
template&
std::string f(T)
{
return "Template";
}
std::string f(int)
{
return "Nontemplate";
}
int main()
{
int x = 7;
std::cout &<&< f(x) &<&< std::endl;
}
這裡我就賣個關子,不給出解釋了,大家也先不要急著到編譯器里去驗證,根據我們前面講述的知識,可以先試著通過思考,回答下面幾個問題:
- 這兩個例子中的函數,在定義能通過編譯嗎?調用時能通過編譯嗎?
- 如果能夠運行的話,編譯器會做出我們期望的重載決議和類型推導嗎?
弄明白了這兩個例子,Q5的問題自然也就得到解答了。
-------------------------------------
好了,通過這一系列的追問,我們總算把相關的行為給解釋清楚了。想清楚了上面這些細節,我們也就可以很輕鬆地認識到標準這麼規定的理由,說穿了非常簡單,就是兩點:
- 始終保證簽名的全局唯一性。
- 始終保證同一個模板的本體和其所有的特化,在簽名上的相關性。
- 條目1使得函數簽名這個機制被用於函數重載的決議成為可能
- 條目2使得函數簽名這個機制被用於模板特化時的類型推導成為可能
-------------------------------------
嗯,這個問題還是蠻有趣的,不知不覺也討論了這麼多。
應該沒有落下什麼吧。那麼先這樣吧,有問題的話再補充。
[ 注 ] 本文同時發在我的 blog (由於對 markdown 的支持,在那裡或許可獲得更好的閱讀體驗)[知乎] C++ template 為什麼不能推導返回值類型?C++14 可以:
template&
要讓返回值參與「重載解析」,需要一點「奇技淫巧」,具體請參考:怎樣讓C++函數重載時連返回值類型也加入重載決議?
關於return type自動推導的問題,請參見C++14。只能說C++11沒來得及做這個部分。在有函數體的時候編譯器是有能力推導返回值結果的。
T沒被參數用到的一般要寫成這樣才能自動推導,譬如說
template&
auto Add(Fuck fuck, Shit shit)-&>decltype(fuck+shit)
{
return fuck + shit;
}
我想到一個類似的問題:函數重載的時候,為什麼不能重載返回類型不一樣的函數,例如,下面是非法的:
int dosth(int p1);
bool dosth(int p1);
dosth(10);
此時,編譯器根本無從推導到底該用哪個函數。
模板函數返回值推導同理。你的寫法肯定是錯的。你的 T 是推導不出來,因為 C++ 中的模板函數,模板參數是由函數參數來推導的,而不是函數的返回值。而你的函數參數是空的。
你這種寫法只有外部調用的時候,顯式指定模板參數類型才能通過,比如這樣:int i = value&
但我想這不是你本意吧?
#include &
using namespace std;
template &
T function() {
return T();
}
int main() { 這樣可以。
cout &<&< function&
推薦閱讀:
※什麼編譯器優化技術可以把FP語言里的sum [1..n]的效率優化到C語言的水平?
※即時編譯器與解釋器的區別?
※設計類Python編譯器時如何處理tab和space縮進?
※為什麼所有的教科書中都不贊成手寫自底向上的語法分析器?
※學好c++,是不是最好研究下其編譯器?因為感覺c++的編譯器做了很多僅從語言前端看不出的工作。?