聊聊C++模板函數與非模板函數的重載

前言

函數重載在C++中是一個很重要的特性。之因此有了它纔有了操做符重載、iostream、函數子、函數適配器、智能指針等很是有用的東西。ios

日常在實際的應用中多半要麼是模板函數與模板函數重載,或者是非模板函數與非模板重載。而讓模板函數與非模板函數重載的狀況卻不多。數組

前段時間在項目中偶然遇到了一個模板函數與非模板函數重載的詭異問題,大概至關於下面這種狀況:ide

template <typename T> 
int compare(const T& lhs, const T& rhs)
{
    std::cout << "template compare" << std::endl;
    return 0;
}

int compare(const char* lhs, const char* rhs)
{
    std::cout << "ordinary compare" << std::endl;
    return 0;
}

int main(int argc, char *argv[])
{
    char c1[] = "hello";
    char c2[] = "hello";
    compare(c1, c2);
}

最終輸出打印的是什麼呢?嗯哼?函數

分析

開始的時候我覺得理所固然輸出的是「ordinary compare」,就沒有在乎這裏。結果在程序的其餘地方調試了好久死活找不出問題的所在,而後索性就把那個非模板函數改爲了模板函數的偏特化函數,以前出現的問題就消失了。這才發現問題出如今以前的模板函數與非模板函數重載那裏了。那時候的狀況就跟上面的代碼的狀況差很少一個意思。ui

回到上面代碼輸出的打印結果上來,在幾個主流的編譯器上的輸出結果是這樣的:this

  • g++ 4.8.1 : template compare
  • clang 3.4.2 : template compare
  • vs2010 :ordinary compare

先來看看C++中模板函數與非模板函數的重載決議步驟:spa

1.  爲這個函數名創建候選函數集合,包括:指針

  • 與被調用函數名字相同的任意普通函數。
  • 任意函數模板實例化,在其中,模板實參推斷髮現了與調用中所用函數實參相匹配的模板實參。

2.  肯定哪些普通函數是可行的(若是有可行函數的話)。候選集合中的每一個模板實例均可行的,由於模板實參推斷保證函數能夠被調用。調試

3.  若是須要轉換來進行調用,根據轉換的種類排列可靠函數,記住,調用模板函數實例所容許的轉換是有限的。code

  • 若是隻有一個函數可選,就調用這個函數。
  • 若是調用有二義性,從可行函數集合中去掉全部函數模板實例。

4.  從新排列去掉函數模板實例的可行函數。

  • 若是隻有一個函數可選,就調用這個函數。
  • 不然,調用有二義性。

再說說爲何我一開始認爲必定是輸出「ordinary compare」。數組c一、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是將他們轉換爲指向數組首元素的指針後再進行匹配的,因此它選擇非模板函數。

那麼到底哪一個比較正確呢?

咱們來看看模板實參推斷時對實參的轉換規則。

通常而論,不會轉換實參以匹配已有的實例化,相反,會產生新的實例。除了產生新的實例化以外,編譯器只會執行兩種轉換:

  • const 轉換:接受 const 引用或 const 指針的函數能夠分別用非 const對象的引用或指針來調用,無須產生新的實例化。若是函數接受非引用類型,形參類型實參都忽略const,即,不管傳遞 const 或非 const 對象給接受非引用類型的函數,都使用相同的實例化。
  • 數組或函數到指針的轉換:若是模板形參不是引用類型,則對數組或函數類型的實參應用常規指針轉換。數組實參將看成指向其第一個元素的指針,函數實參看成指向函數類型的指針。

按照實參推導時實參轉換規則,由於模板函數的實參是引用類型,不會對數組實參進行到指針的轉換,因此直接推斷T = typename [n](typename爲數組的類型,n爲數組的長度)。對於本文的狀況,模板函數直接進行實參推斷並匹配,即T = char  [6],而非模板函數先要將數組轉換爲指針,再匹配函數。因此我認爲正確的應該是匹配模板函數。若是令文中的數組c1和c2的長度不同,那麼模板函數兩個實參推斷結果不同而致使匹配失敗,進而應該匹配非模板函數

最後說一下,在實際應用中的大多數狀況都應該用模板函數與模板函數的偏特化來代替模板函數與普通非模板函數的重載,以免模板函數與非模板函數的重載致使在不一樣編譯器環境下結果不同的狀況發生。

參考文獻

  1. Stanley B.Lippman. C++ Primer, 4th Edition. 人民郵電出版社, 2006

(完)

相關文章
相關標籤/搜索