前言

函数重载在C++中是一个很重要的特性。之所以有了它才有了操作符重载、iostream、函数子、函数适配器、智能指针等非常有用的东西。

平常在实际的应用中多半要么是模板函数与模板函数重载,或者是非模板函数与非模板重载。而让模板函数与非模板函数重载的情况却很少。

前段时间在项目中偶然遇到了一个模板函数与非模板函数重载的诡异问题,大概相当于下面这种情况:

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”,就没有在意这里。结果在程序的其他地方调试了很久死活找不出问题的所在,然后索性就把那个非模板函数改成了模板函数的偏特化函数,之前出现的问题就消失了。这才发现问题出现在之前的模板函数与非模板函数重载那里了。那时候的情况就跟上面的代码的情况差不多一个意思。

回到上面代码输出的打印结果上来,在几个主流的编译器上的输出结果是这样的:

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

先来看看C++中模板函数与非模板函数的重载决议步骤:

1.  为这个函数名建立候选函数集合,包括:

  • 与被调用函数名字相同的任意普通函数。
  • 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。

2.  确定哪些普通函数是可行的(如果有可行函数的话)。候选集合中的每个模板实例都可行的,因为模板实参推断保证函数可以被调用。

3.  如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。

  • 如果只有一个函数可选,就调用这个函数。
  • 如果调用有二义性,从可行函数集合中去掉所有函数模板实例。

4.  重新排列去掉函数模板实例的可行函数。

  • 如果只有一个函数可选,就调用这个函数。
  • 否则,调用有二义性。

再说说为什么我一开始认为一定是输出“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是将他们转换为指向数组首元素的指针后再进行匹配的,所以它选择非模板函数。

那么到底哪个比较正确呢?

我们来看看模板实参推断时对实参的转换规则。

一般而论,不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:

  • 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

(完)

聊聊C++模板函数与非模板函数的重载的更多相关文章

  1. 多线程中,static函数与非static函数的区别?

    最近在学习多线程,刚入门,好多东西不懂,下面这段代码今天想了半天也没明白,希望看到的兄弟姐妹能解释下. public class NotThreadSafeCounter extends Thread ...

  2. const和非const函数重载

    成员函数后面加const,表示在该函数中不能对类的数据成员进行改变,比如下面的代码: #include <stdio.h> class A { private: mutable int a ...

  3. c++ 虚函数和纯虚函数

    在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的.从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现.通过这样的方法,就可以将对象 ...

  4. C++多态、虚函数、纯虚函数、抽象类

    多态 同一函数调用形式(调用形式形同)可以实现不同的操作(执行路径不同),就叫多态. 两种多态: (1)静态多态:分为函数重载和运算符重载,编译时系统就能决定调用哪个函数. (2)动态多态(简称多态) ...

  5. C++中模板类使用友元模板函数

    在类模板中可以出现三种友元声明:(1)普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数.(2)类模板或函数模板的友元声明,授予对友元所有实例的访问权.(3)只授予对类模板或函数模板的特定 ...

  6. C++—模板(1)模板与函数模板

    1.引入 如何编写一个通用加法函数?第一个方法是使用函数重载, 针对每个所需相同行为的不同类型重新实现这个函数.C++的这种编程机制给编程者极大的方便,不需要为功能相似.参数不同的函数选用不同的函数名 ...

  7. C++模板类内友元(友元函数,友元类)声明的三种情况

    根据<C++ Primer>第三版16.4节的叙述,C++类模板友元分为以下几种情况 1.非模板友元类或友元函数. 书上给了一个例子: class Foo{     void bar(); ...

  8. 【C++编程基础】(1)—— 函数原型声明、函数模板、引用、const 常引用、const 常量指针

    一.函数原型声明: 1.函数声明告诉编译器函数的名称,和如何调用函数(返回类型和参数):函数定义提供了函数的实际主体. 2.强制性的:在C++中,如果函数调用的位置在函数定义之前,则要求在函数调用之前 ...

  9. C++ enable_if 模板特化实例(函数返回值特化、函数参数特化、模板参数特化、模板重载)

    1. enable_if 原理 关于 enable_if 原理这里就不细说了,网上有很多,可以参考如下教程,这里只讲解用法实例,涵盖常规使用全部方法. 文章1 文章2 文章3 1. 所需头文件 #in ...

随机推荐

  1. gcc options选项的优化及选择

    gcc options选项的优化 -c和-o都是gcc编译器的可选参数[options] -c表示只编译(compile)源文件但不链接,会把.c或.cc的c源程序编译成目标文件,一般是.o文件.[只 ...

  2. 初学者学习python2还是python3?

    如果你是一个初学者,或者你以前接触过其他的编程语言,你可能不知道,在开始学习python的时候都会遇到一个比较让人很头疼的问题:版本问题!!是学习python2 还是学习 python3 ?这是非常让 ...

  3. 201621044079 week07-JAVA GUI类

    作业07-Java GUI编程 1. 本周学习总结 1.1 思维导图:Java图形界面总结 1.2 可选:使用常规方法总结其他上课内容. 2.书面作业 1. GUI中的事件处理 1.1 写出事件处理模 ...

  4. 201621044079 week05-继承、多态、抽象类与接口

    作业05-继承.多态.抽象类与接口 1. 本周学习总结 1.1 写出你认为本周学习中比较重要的知识点关键词 接口 interface关键字 implements has-a;comparable co ...

  5. System and Device power management.

    Advanced Configuration and Power Management Interface(ACPI)是由Intel,Microsoft等厂家订的一套Spec,规范了OS,APP对于电 ...

  6. Delphi函数详解:全局函数,内部函数,类的成员函数,类的静态方法

    1. Delphi中的全局函数 //要点: 需要给其他单元调用, 必须在 interface 声明, 但必须在 uses 区后面 unit Unit1; interface uses   Window ...

  7. Eclipse打不开,闪退

    自己编写了个程序,运行巨慢..无语,输出太多,后来冒出一个错误,不知什么原因啊,再后来Eclipse就打不开了,到workbench闪退... 百度后解决方案: 进入目录:workspace/.met ...

  8. BZOJ day2

    十六题...(好难啊) 1051105910881191119214321876195119682242243824562463276128184720

  9. Codeforces Round #526 (Div. 2) C. The Fair Nut and String

    C. The Fair Nut and String 题目链接:https://codeforces.com/contest/1084/problem/C 题意: 给出一个字符串,找出都为a的子序列( ...

  10. springboot之mybatis别名的设置

    mybatis别名设置 在具体的mapper.xml文件中,定义很多的statement,statement需要parameterType指定输入参数的类型.需要resultType指定输出结果的映射 ...