为什么编译器在这种情况下选择了不正确的函数重载

Why does the compiler choose the incorrect function overload in this case?

本文关键字:不正确 函数 重载 选择 编译器 这种情况下 为什么      更新时间:2023-10-16

我正在尝试Sean Parent在GoingNative 2013演讲中提出的代码——"继承是邪恶的基类"。(上一张幻灯片中的代码,位于https://gist.github.com/berkus/7041546

我试着自己实现同样的目标,但我不明白为什么下面的代码不会像我期望的那样发挥作用。

#include <boost/smart_ptr.hpp>
#include <iostream>
#include <ostream>
template <typename T>
void draw(const T& t, std::ostream& out)
{
    std::cout << "Template version" << 'n';
    out << t << 'n';
}
class object_t
{
public:
    template <typename T>
    explicit object_t (T rhs) : self(new model<T>(rhs)) {};
    friend void draw(const object_t& obj, std::ostream& out)
    {
        obj.self->draw(out);
    }
private:
    struct concept_t
    {
        virtual ~concept_t() {};
        virtual void draw(std::ostream&) const = 0;
    };
    template <typename T>
    struct model : concept_t
    {
        model(T rhs) : data(rhs) {};
        void draw(std::ostream& out) const
        {
            ::draw(data, out);
        }
        T data;
    };
    boost::scoped_ptr<concept_t> self;
};
class MyClass {};
void draw(const MyClass&, std::ostream& out)
{
    std::cout << "MyClass version" << 'n';
    out << "MyClass" << 'n';
}
int main()
{
    object_t first(1);
    draw(first, std::cout);
    const object_t second((MyClass()));
    draw(second, std::cout);
    return 0;
}

这个版本可以很好地处理打印int,但在第二种情况下无法编译,因为编译器不知道如何将MyClassoperator<<一起使用。我不明白编译器为什么不选择专门为MyClass提供的第二个重载。如果我更改model::draw()方法的名称并从其主体中删除::全局命名空间说明符,或者如果我将MyClass的draw全局函数更改为完整的模板专用化,则代码会编译并运行良好。

我得到的错误消息如下,之后是一堆candidate function not viable...

t76_stack_friend_fcn_visibility.cpp:9:9: error: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'const MyClass')
    out << t << 'n';
    ~~~ ^  ~
t76_stack_friend_fcn_visibility.cpp:36:15: note: in instantiation of function template specialization 'draw<MyClass>' requested here
            ::draw(data, out);
              ^
t76_stack_friend_fcn_visibility.cpp:33:9: note: in instantiation of member function 'object_t::model<MyClass>::draw' requested here
        model(T rhs) : data(rhs) {};
        ^
t76_stack_friend_fcn_visibility.cpp:16:42: note: in instantiation of member function 'object_t::model<MyClass>::model' requested here
    explicit object_t (T rhs) : self(new model<T>(rhs)) {};
                                         ^
t76_stack_friend_fcn_visibility.cpp:58:20: note: in instantiation of function template specialization 'object_t::object_t<MyClass>' requested here
    const object_t second((MyClass()));
                   ^

为什么选择全局绘制模板函数的模板版本而不是MyClass函数重载?是因为模板引用是贪婪的吗?如何解决此问题?

因为在函数调用中使用了限定名称。[temp.dep.candidate]:

对于依赖于模板参数的函数调用使用通常的查找规则(3.4.1,3.4.2、3.4.3),但除外

  • 对于使用非限定名称查找(3.4.1)或限定名称查找的查找部分(3.4.3)找到模板定义上下文
  • 对于使用关联名称空间的查找部分(3.4.2),只有在模板定义中找到的函数声明上下文或模板实例化上下文

§3.4.2(别名〔basic.lookup.argdep〕):

当函数调用(5.2.2)中的后缀表达式不合格id时,可以搜索在通常的不合格查找(3.4.1>)过程中未考虑的其他命名空间,并且在这些命名空间中,命名空间作用域友元函数声明(11.3)可以找到可见的。

因此,本质上ADL不适用,因为调用使用了限定id
正如Barry在回答中所示,您可以通过使呼叫不合格来解决此问题:

void draw(std::ostream& out) const
{
    using ::draw;
    draw(data, out);
}

不过,您必须在此之前添加一个using-声明。否则,在按升序搜索声明性区域时,非限定名称查找将首先找到model<>::draw成员函数,并且不会再进行搜索。但不仅如此,因为model<>::draw(它是一个类成员)被我的非限定名称查找发现,ADL被没有调用,[basic.lookup.argdep]/3:

X为非限定查找(3.4.1.)产生的查找集设Y为参数相关查找生成的查找集(定义如下)。如果X包含

  • 类成员的声明,或
  • 块作用域函数声明不是using声明,或者
  • 既不是函数也不是函数模板的声明

则CCD_ 13为空。否则,Y是在与参数类型,如下所述。

因此,如果提供了using声明,则通过非限定名称查找找到的唯一声明将是引入model::draw声明性区域的全局draw模板。ADL然后被调用,并为CCD_ 19找到后来声明的CCD_。

当您直接调用::draw()时,您无法正确使用ADL。(为什么?我实际上不知道具体情况,希望有人也能进来向我解释这一点[编辑:请参阅Columbo的答案和原因])但为了真正使用ADL,你需要像这样对draw打一个无条件的电话:

void draw(std::ostream& out) const
{
    using ::draw;
    draw(data, out);
}

这将正确地找到过载draw(const MyClass&, std::ostream&)