函數重載在C++中是一個很重要的特性。之所以有了它才有了操作符重載、iostream、函數子、函數適配器、智能指針等非常有用的東西。
平常在實際的應用中多半要麼是模板函數與模板函數重載,或者是非模板函數與非模板重載。而讓模板函數與非模板函數重載的情況卻很少。
前段時間在項目中偶然遇到了一個模板函數與非模板函數重載的詭異問題,大概相當於下面這種情況:
1 template <typename T> 2 int compare(const T& lhs, const T& rhs) 3 { 4 std::cout << "template compare" << std::endl; 5 return 0; 6 } 7 8 int compare(const char* lhs, const char* rhs) 9 { 10 std::cout << "ordinary compare" << std::endl; 11 return 0; 12 } 13 14 int main(int argc, char *argv[]) 15 { 16 char c1[] = "hello"; 17 char c2[] = "hello"; 18 compare(c1, c2); 19 }
最終輸出打印的是什麼呢?嗯哼?
開始的時候我以為理所當然輸出的是“ordinary compare”,就沒有在意這裡。結果在程序的其他地方調試了很久死活找不出問題的所在,然後索性就把那個非模板函數改成了模板函數的偏特化函數,之前出現的問題就消失了。這才發現問題出現在之前的模板函數與非模板函數重載那裡了。那時候的情況就跟上面的代碼的情況差不多一個意思。
回到上面代碼輸出的打印結果上來,在幾個主流的編譯器上的輸出結果是這樣的:
g++ 4.8.1 : template compare
clang 3.4.2 : template compare
vs2010 :ordinary compare
先來看看C++中模板函數與非模板函數的重載決議步驟:
1、為這個函數名建立候選函數集合,包括:
a、與被調用函數名字相同的任意普通函數。
b、任意函數模板實例化,在其中,模板實參推斷發現了與調用中所用函數實參相匹配的模板實參。
2、確定哪些普通函數是可行的(如果有可行函數的話)。候選集合中的每個模板實例都可行的,因為模板實參推斷保證函數可以被調用。
3、如果需要轉換來進行調用,根據轉換的種類排列可靠函數,記住,調用模板函數實例所允許的轉換是有限的。
a、如果只有一個函數可選,就調用這個函數。
b、如果調用有二義性,從可行函數集合中去掉所有函數模板實例。
4、重新排列去掉函數模板實例的可行函數。
a、如果只有一個函數可選,就調用這個函數。
b、否則,調用有二義性。
再說說為什麼我一開始認為一定是輸出“ordinary compare”。數組c1、c2要作為實參傳參給函數的形參的話要轉換為指向數組首元素的指針,也就是說對於模板函數和非模板函數來說都要經過一次轉換才能完全匹配,那麼根據上面的重載決議規則,就應該調用非模板函數。但結果卻並非如此。
這個問題當時在知乎問過,來看看陳碩老師的回答:
C++ 這套重載決議規則太復雜,g++/clang 都是resolve為模板,具現化後的模板是: int compare<char [6]>(char const (&) [6], char const (&) [6]) 也就是說T = char[6],數組沒有轉化為指針。 如果把其中一個"hello"改成別的長度的字符串,就是匹配普通版本了。 如果g++/clang是符合標准的話,我傾向於認為這是C++標准的bug。 FYI, clang consider template is better because it's an Identity Conversion, the other is array-to-pointer: #1 clang::compareStandardConversionSubsets (Context=..., SCS1=..., SCS2=...) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:3393 #1 0x00007ffff6cfb353 in clang::CompareStandardConversionSequences (S=..., SCS1=..., SCS2=...) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:3469 #2 0x00007ffff6cfaeff in clang::CompareImplicitConversionSequences (S=..., ICS1=..., ICS2=...) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:3336 #3 0x00007ffff6d0ac37 in clang::isBetterOverloadCandidate (S=..., Cand1=..., Cand2=..., Loc=..., UserDefinedConversion=false) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:8031 #4 0x00007ffff6d0afd1 in clang::OverloadCandidateSet::BestViableFunction (this=0x7fffffff77a0, S=..., Loc=..., Best=@0x7fffffff7790: 0x7fffffff7860, UserDefinedConversion=false) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:8148 #5 0x00007ffff6d12220 in clang::Sema::BuildOverloadedCallExpr (this=0x7445e0, S=0x781630, Fn=0x782620, ULE=0x782620, LParenLoc=..., Args=..., RParenLoc=..., ExecConfig=0x0, AllowTypoCorrection=true) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaOverload.cpp:10394 #6 0x00007ffff6bceb9a in clang::Sema::ActOnCallExpr (this=0x7445e0, S=0x781630, Fn=0x782620, LParenLoc=..., ArgExprs=..., RParenLoc=..., ExecConfig=0x0, IsExecConfig=false) at llvm-3.4.2.src/tools/clang/lib/Sema/SemaExpr.cpp:4470 #7 0x00007ffff7255459 in clang::Parser::ParsePostfixExpressionSuffix (this=0x75f6f0, LHS=...) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseExpr.cpp:1455 #8 0x00007ffff7254925 in clang::Parser::ParseCastExpression (this=0x75f6f0, isUnaryExpression=false, isAddressOfOperand=false, NotCastExpr=@0x7fffffffa59f: false, isTypeCast=clang::Parser::NotTypeCast) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseExpr.cpp:1279 #9 0x00007ffff7251fba in clang::Parser::ParseCastExpression (this=0x75f6f0, isUnaryExpression=false, isAddressOfOperand=false, isTypeCast=clang::Parser::NotTypeCast) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseExpr.cpp:419 #10 0x00007ffff7251105 in clang::Parser::ParseAssignmentExpression (this=0x75f6f0, isTypeCast=clang::Parser::NotTypeCast) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseExpr.cpp:168 #11 0x00007ffff7250f2c in clang::Parser::ParseExpression (this=0x75f6f0, isTypeCast=clang::Parser::NotTypeCast) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseExpr.cpp:120 #12 0x00007ffff727ba85 in clang::Parser::ParseExprStatement (this=0x75f6f0) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseStmt.cpp:371 #13 0x00007ffff727b46b in clang::Parser::ParseStatementOrDeclarationAfterAttributes (this=0x75f6f0, Stmts=..., OnlyStatement=false, TrailingElseLoc=0x0, Attrs=...) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseStmt.cpp:231 #14 0x00007ffff727abae in clang::Parser::ParseStatementOrDeclaration (this=0x75f6f0, Stmts=..., OnlyStatement=false, TrailingElseLoc=0x0) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseStmt.cpp:118 #15 0x00007ffff727d7c8 in clang::Parser::ParseCompoundStatementBody (this=0x75f6f0, isStmtExpr=false) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseStmt.cpp:907 #16 0x00007ffff7283373 in clang::Parser::ParseFunctionStatementBody (this=0x75f6f0, Decl=0x782340, BodyScope=...) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseStmt.cpp:2458 #17 0x00007ffff7223ada in clang::Parser::ParseFunctionDefinition (this=0x75f6f0, D=..., TemplateInfo=..., LateParsedAttrs=0x7fffffffb8f0) at llvm-3.4.2.src/tools/clang/lib/Parse/Parser.cpp:1171 #18 0x00007ffff7230a62 in clang::Parser::ParseDeclGroup (this=0x75f6f0, DS=..., Context=0, AllowFunctionDefinitions=true, DeclEnd=0x0, FRI=0x0) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseDecl.cpp:1617 #19 0x00007ffff7222b6b in clang::Parser::ParseDeclOrFunctionDefInternal (this=0x75f6f0, attrs=..., DS=..., AS=clang::AS_none) at llvm-3.4.2.src/tools/clang/lib/Parse/Parser.cpp:932 #20 0x00007ffff7222c33 in clang::Parser::ParseDeclarationOrFunctionDefinition (this=0x75f6f0, attrs=..., DS=0x0, AS=clang::AS_none) at llvm-3.4.2.src/tools/clang/lib/Parse/Parser.cpp:948 #21 0x00007ffff72223bb in clang::Parser::ParseExternalDeclaration (this=0x75f6f0, attrs=..., DS=0x0) at llvm-3.4.2.src/tools/clang/lib/Parse/Parser.cpp:807 #22 0x00007ffff72218ab in clang::Parser::ParseTopLevelDecl (this=0x75f6f0, Result=...) at llvm-3.4.2.src/tools/clang/lib/Parse/Parser.cpp:612 #23 0x00007ffff721e1e5 in clang::ParseAST (S=..., PrintStats=false, SkipFunctionBodies=false) at llvm-3.4.2.src/tools/clang/lib/Parse/ParseAST.cpp:144
也就是說g++和clang選擇匹配模板函數,是因為它們並沒有將c1和c2轉換為指向數組首元素的指針,而是直接匹配,即T = char [6]。而VS2010是將他們轉換為指向數組首元素的指針後再進行匹配的,所以它選擇非模板函數。
那麼到底哪個比較正確呢?
我們來看看模板實參推斷時對實參的轉換規則。
一般而論,不會轉換實參以匹配已有的實例化,相反,會產生新的實例。除了產生新的實例化之外,編譯器只會執行兩種轉換:
1、const 轉換:接受 const 引用或 const 指針的函數可以分別用非 const對象的引用或指針來調用,無須產生新的實例化。如果函數接受非引用類型,形參類型實參都忽略const,即,無論傳遞 const 或非 const 對象給接受非引用類型的函數,都使用相同的實例化。
2、數組或函數到指針的轉換:如果模板形參不是引用類型,則對數組或函數類型的實參應用常規指針轉換。數組實參將當作指向其第一個元素的指針,函數實參當作指向函數類型的指針。
按照實參推導時實參轉換規則,因為模板函數的實參是引用類型,不會對數組實參進行到指針的轉換,所以直接推斷T = typename [n](typename為數組的類型,n為數組的長度)。對於本文的情況,模板函數直接進行實參推斷並匹配,即T = char [6],而非模板函數先要將數組轉換為指針,再匹配函數。所以我認為正確的應該是匹配模板函數。如果令文中的數組c1和c2的長度不一樣,那麼模板函數兩個實參推斷結果不一樣而導致匹配失敗,進而應該匹配非模板函數
最後說一下,在實際應用中的大多數情況都應該用模板函數與模板函數的偏特化來代替模板函數與普通非模板函數的重載,以避免模板函數與非模板函數的重載導致在不同編譯器環境下結果不一樣的情況發生。
(完)