【问题标题】:C++: overloading does not choose expected methodC++:重载不选择预期的方法
【发布时间】:2012-03-29 18:47:32
【问题描述】:

我有以下代码:

#include <iostream>
#include <vector>
using namespace std;

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

template <typename T>
void foo(const T& obj) { cerr << "Generic case"<< endl;}

void foo(const A& a) {
    cerr << "Specific case" << endl;
}

int main() {
    vector<int> v;
    foo(v);
    B b;
    foo(b);
    A a;
    foo(a);
}

输出是

  • 一般情况
  • 一般情况
  • 具体情况

为什么没有为B 对象选择foo(const A&amp; a)

奇怪的是,如果我删除了模板化的方法,只剩下以下内容:

#include <iostream>
#include <vector>

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

//template <typename T>
//void foo(const T& obj) { cerr << "Generic case"<< endl;}

void foo(const A& a) {
    cerr << "Specific case" << endl;
}

int main() {
    B b;
    foo(b);
    A a;
    foo(a);
}

代码编译,输出为:

Specific case
Specific case

为什么模板化方法的存在会产生如此大的影响?

编辑:如何强制编译器在存在的情况下为从 A 派生的类选择免费方法 模板化方法?

【问题讨论】:

    标签: c++ templates inheritance overloading


    【解决方案1】:

    模板实例化产生的对foo(const B&amp;)的调用不需要转换,因此它是更好的匹配。

    当编译器看到函数调用时,每个基本函数模板都必须被实例化,并与每个普通函数一起包含在重载集中。之后执行重载决议。还有 SFINAE,它允许函数模板的实例化导致错误(这样的函数不会被添加到重载集)。当然,事情并没有那么简单,但它应该可以给出大致的情况。

    关于您的编辑:只有一种方法可以调用。还能输出什么?

    【讨论】:

    • 是的——只有一种方法可以简化事情。我想我预计在这种情况下 const B& 调用也会失败。
    • BA 的直接子代。它可以绑定到A&amp;const A&amp;
    • 现在我意识到我已经花了大约 31 分钟来撰写我的答案......所以吃我的时间:x
    • @MatthieuM。我为你感到难过。如果有 Sophia-Antipolis SO 会议,我会请你喝啤酒;)
    【解决方案2】:

    是的,这有点令人惊讶,但是在重载解析方面继承和模板并不能很好地混合。

    问题是,在评估应该选择哪个重载时,编译器会选择需要最少转换的那个(内置到内置、派生到基、对非显式构造函数或转换运算符的调用,等等...)。排名算法实际上相当复杂(并非所有转化都被同等对待...)。

    一旦重载被排序,如果两个最上面的排序相同并且一个是模板,则模板被丢弃。但是,如果模板的排名高于非模板(通常转化次数较少),则选择模板。

    在你的情况下:

    • 对于std::vector&lt;int&gt;,只有一个重载匹配,因此被选中。
    • 对于A 两个重载匹配,它们的排名相同,模板之一被丢弃。
    • 对于B 两个重载匹配,模板排名更高(不需要派生到基址的转换),它被选中。

    有两种解决方法,最简单的是“修复”调用站点:

    A const& ba = b;
    foo(ba);
    

    另一个是修复模板本身,但这比较棘手......

    您可以对派生自 A 的类进行硬编码,这不是您想要的重载:

    template <typename T>
    typename std::enable_if<not std::is_base_of<A, T>::value>::type
    foo(T const& t) {
      std::cerr << "Generic case\n";
    }
    

    但是这不是那么灵活...

    另一个解决方案是定义一个钩子。首先我们需要一些元编程工具:

    // Utility
    template <typename T, typename Result = void>
    struct enable: std::enable_if< std::is_same<T, std::true_type>::value > {}; 
    
    template <typename T, typename Result = void>
    struct disable: std::enable_if< not std::is_same<T, std::true_type>::value > {}; 
    

    然后我们定义我们的钩子和函数:

    std::false_type has_specific_foo(...);
    
    template <typename T>
    auto foo(T const& t) -> typename disable<decltype(has_specific_foo(t))>::type {
      std::cerr << "Generic case\n";
    }
    

    然后对于每个基类,我们需要一个特定的 foo:

    std::true_type has_specific_foo(A const&);
    

    ideone 正在行动。

    在 C++03 中也可以,但稍微麻烦一些。想法是一样的,省略号参数... 的排名最差,因此我们可以在另一个函数上使用重载选择来驱动主函数的选择。

    【讨论】:

      【解决方案3】:

      @pmr 的answer 解释了为什么在您的示例中首选模板化函数。要强制编译器选择您的重载,您可以使用 SFINAE 从重载集中删除模板化函数。将模板化的foo 更改为

      template <typename T>
      typename std::enable_if<!std::is_base_of<A, T>::value>::type
        foo(const T& obj) { cerr << "Generic case"<< endl;}
      

      现在,如果 TA 或派生自 A 的类,则模板化函数的返回类型无效,它将被排除在重载决议之外。 enable_if 存在于 type_traits 标头中。

      【讨论】:

      • 难道我也必须从 foo() 返回一些东西吗?如果我想保持方法签名相同怎么办。是否可以更改 foo() 内部的代码并使用 enable_if 来防止由于 foo() 的方法体而实例化?
      • @ATemp enable_if 有 2 个模板参数,第二个是返回类型。它默认为void,这就是我在示例中省略它的原因。如果您有不同的返回类型,只需添加第二个模板参数。由于其主体中的代码,您无法阻止在重载解析期间选择模板化函数。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-07-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多