问题 在这种情况下,为什么编译器会选择不正确的函数重载?


我正在尝试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 很好,但在第二种情况下无法编译,因为编译器不知道如何使用 MyClass 同 operator<<。我无法理解为什么编译器不会选择专门为其提供的第二个重载 MyClass。如果我更改model :: draw()方法的名称并删除,则代码编译并正常工作 :: 来自其主体的全局命名空间说明符,或者如果我将MyClass'绘制全局函数更改为完整的模板特化。

我得到的错误信息如下,之后是一堆 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函数重载?是因为模板参考是贪婪的吗?如何解决这个问题?


1209
2017-11-08 20:33


起源

我用MSVC13尝试了你的代码并且它编译得很好,在第一种情况下使用int版本,在第二种情况下使用MyClass版本。您应该添加有关您正在使用的编译器的信息 - Christophe
我使用clang --version clang版本3.5.0(标签/ RELEASE_350 / final)。 - Sebastian Kramer
另一个问题具有相同的原则 - M.M


答案:


因为您在函数调用中使用限定名称。 [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不适用,因为调用使用了qualified-id。
正如巴里所说 在他的回答中 您可以通过使呼叫不合格来解决此问题:

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包含

  • 集体成员的声明, 要么
  • 块范围函数声明不是 使用声明, 要么
  • 既不是函数也不是函数模板的声明

然后 Y 是空的。除此以外 Y 是与该关联的名称空间中找到的声明集   参数类型如下所述。

因此,如果 using-declaration是唯一由非限定名称查找的声明将是全局的 draw 引入声明区域的模板 model::draw。 然后调用ADL并找到后面声明的 draw 功能 MyClass const&


8
2017-11-08 20:48



你知道第一个要点的理由吗? - Sebastian Kramer
@SebastianKramer这就是所谓的“两阶段查找”。考虑 void foo(void *); template<class T> void bar(T /*dummy*/){ foo(0); } void foo(int); int main(){ bar(0); } 大概是模板的作者 bar 打电话给 void * 超载而不是 int 超载。 - T.C.
@ T.C。你确定它与它一起工作的事实 using 声明是否符合标准?我对[namespace.udecl] / 11有点担心 - Columbo
@SebastianKramer第一个项目符号的理由:这是确保在模板定义之后不搜索不依赖于模板参数的名称。因为非依赖名称的名称查找应该与通常的函数一样。还要考虑[temp.nondep]:“使用通常的名称查找找到模板定义中使用的非依赖名称,并在它们被使用时绑定。” - Columbo
@Columbo但你并不依赖于此 using 引入过载带来的 MyClass &。你依赖于ADL。 - T.C.


当你直接打电话 ::draw(),你无法正确使用ADL。 (为什么?我实际上并不具体了解,希望有人会进来向我解释这一点 [编辑:见 科伦坡的回答 为什么])但是为了实际使用ADL,你需要进行无条件的调用 draw 像这样:

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

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


3
2017-11-08 20:49



扩展搜索范围有助于,谢谢。然而,我正在寻找这种行为背后的理性。 - Sebastian Kramer
@SebastianKramer很好,ADL不能在定义上下文中应用,因为我们不知道参数或候选函数是什么。并且期望ADL确实适用于实例化上下文(出于与之相同的原因) ADL在模板之外是可取的)。这使我们处于现状。 - M.M
虽然我还没有看到为什么3.4.1查找不应该在依赖函数调用的实例化上下文中应用。 - M.M