【问题标题】:Overload resolution, templates and inheritance重载解析、模板和继承
【发布时间】:2015-02-09 08:56:01
【问题描述】:
#include <iostream>

struct A {};
struct B : public A {};

template<typename T>
void foo(const T &x) { std::cout << "Called template" << std::endl; }

void foo(const A &a) { std::cout << "Called A" << std::endl; }

int main()
{
    foo(A());
    foo(B());
    return 0;
}

打印出来:

Called A
Called template

我的印象是总是会选择合适的非模板函数而不是模板函数。有人可以向我解释导致这个有点令人惊讶的结果的解决步骤吗?

【问题讨论】:

  • 好吧,这里有一个猜测:B 继承自 A,但它不是 A。因此,当编译器执行重载决议时,它首先寻找一个接受B 的函数,而void foo(const A &amp;a) 不符合这个标准。但是,模板函数确实适合它,因为T 可以成为 B。所以,void foo(const T &amp;x) 是首选。无论如何,这与overload-resolution 标签描述完美匹配:"... 它的规则错综复杂,而且常常令人惊讶,即使对于有经验的用户也是如此。" 所以,确实如此 =)

标签: c++ templates overload-resolution


【解决方案1】:

我的印象是总是会选择合适的非模板函数而不是模板函数。

这仅在模板和非模板是同样好的候选者时才成立。这就是foo(A())选择非模板的原因。

但是,在foo(B()) 的情况下,使用非模板需要派生到基础的转换。所以严格来说,函数模板更好,因此选择了它。

foo 模板实例化为void foo(const B&amp;)。考虑一下没有模板会是什么样子:

void foo(const B &x) { std::cout << "Called template" << std::endl; }

void foo(const A &a) { std::cout << "Called A" << std::endl; }

我相信您会同意致电foo(B()) 应该明确选择第一个。这正是选择模板的原因。

【讨论】:

  • 我想知道的是,是否有一种方法可以编写类似于上述模板重载的重载,该重载仅被选为绝对的最后恢复,即后备。
  • @EmilEriksson 这值得一些研究,然后是一个单独的问题。在我的脑海中,我唯一能想到的就是脆弱的 SFINAE:foo(const T&amp;, sfinae_if&lt;T is not derived from A and T is not ....&gt;)
  • 我同意,我想我会提出这样的问题。很遗憾,您的建议不可扩展。
【解决方案2】:

n3376 13.3.3.1/6

当参数具有类类型并且参数表达式具有 派生类类型,隐式转换序列是 从派生类到基类的派生到基类的转换。

n3376 13.3.3.1/8

如果不需要转换即可将参数与参数匹配 类型,隐式转换序列为标准转换 由身份转换(13.3.3.1.1)组成的序列。

身份转换在 13.3.3.1.1/表 12 中具有精确匹配排名到期表,但派生到基比身份更差。

所以,编译器在第一种情况下只有候选者

// template after resolving
void foo(const A&)

// non-template
void foo(const A&)

两者都有身份等级,但由于第一个是函数模板,因此将选择第二个。 在第二种情况下

// template after resolving
void foo(const B&)

// non-template
void foo(const A&)

只有第一个有身份等级,才会被选中。

【讨论】:

    【解决方案3】:

    有人可以向我解释导致这个有点令人惊讶的结果的解决步骤吗?

    您可以在 cppreference.com 上查看 过载分辨率http://en.cppreference.com/w/cpp/language/overload_resolution

    具体参见隐式转换序列的排名

    部分

    答案的扩展:

    我试图通过上述链接中的信息摘录来提供更多说明:

    函数模板本身不是类型、函数或任何其他实体。仅包含模板定义的源文件不会生成任何代码。为了使任何代码出现,必须实例化模板:必须确定模板参数,以便编译器可以生成实际的函数(或类,从类模板)。

    为此,编译器经过:

    • 函数模板名称查找
    • 模板参数推导

    到这里,编译器有几个候选函数定义可以处理特定的函数调用。这些候选项是模板函数的实例以及程序中相关的非模板函数定义。

    但你的问题的答案其实就在这里:

    模板参数推导发生在函数模板名称查找(可能涉及依赖于参数的查找)和重载解析之后。

    在模板函数实例化之后执行函数重载解析的事实是代码输出的原因。

    现在您的具体情况通过如下重载解决:

    重载分辨率:

    如果 [previous] 步骤产生多个候选函数,则执行重载决策以选择实际调用的函数。通常,参数与参数最接近的候选函数是被调用的函数。 . . .

    ...
    如果 F1 的所有参数的隐式转换不比 F2 的所有参数的隐式转换差,则 F1 被确定为比 F2 更好的函数,并且
    1) F1 的至少一个参数的隐式转换优于 F2 的该参数的相应隐式转换
    ... .
    .
    .
    隐式转换序列的排名:

    每种类型的标准转换序列都被分配三个等级之一:
    1) 精确匹配:无需转换、左值到右值转换、限定转换、自定义类类型到同一类的转换
    2) 提升:积分提升、浮点提升
    3) 转换:整数转换、浮点转换、浮点整数转换、指针转换、指针到成员的转换、布尔转换、派生类到其基类的自定义转换

    标准转换序列的排名是它所持有的标准转换排名中最差的(最多可能有三个转换)

    将引用参数直接绑定到参数表达式是身份或派生到基础的转换:

    struct Base {};
    struct Derived : Base {} d;
    int f(Base&);    // overload #1
    int f(Derived&); // overload #2
    int i = f(d); // d -> Derived& has rank Exact Match
                  // d -> Base& has rank Conversion
                  // calls f(Derived&)
    

    【讨论】:

    • 这是一个仅链接的答案,除了链接之外,它不包含任何信息。要使其成为有效的 SO 答案,请从链接页面添加相关信息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多