【问题标题】:Why does the compiler choose the incorrect function overload in this case?为什么在这种情况下编译器会选择错误的函数重载?
【发布时间】:2015-01-05 10:57:51
【问题描述】:

我正在试用 Sean Parent 在 GoingNative 2013 演讲中提供的代码 - "Inheritance is the base class of evil".(上一张幻灯片中的代码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&lt;&lt; 一起使用。我不明白为什么编译器不会选择专门为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 函数重载?是因为模板引用是贪婪的吗?如何解决这个问题?

【问题讨论】:

  • 我用 MSVC13 尝试了你的代码,它编译得很好,在第一种情况下使用 int 版本,在第二种情况下使用 MyClass 版本。您应该添加有关您正在使用的编译器的信息
  • 我使用 clang --version clang version 3.5.0 (tags/RELEASE_350/final)。

标签: c++ templates inheritance language-lawyer argument-dependent-lookup


【解决方案1】:

因为您在函数调用中使用了限定名称。 [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) 中的 postfix-expressionunqualified-id 时,其他命名空间在此期间不考虑通常的 unqualified lookup (3.4.1) 可能会被搜索,并且在那些命名空间中, 命名空间范围的友元函数声明(11.3),否则 可以找到可见的。

所以基本上 ADL 不适用,因为调用使用了限定 ID。
正如 Barry 显示的 in his answer,您可以通过使呼叫不合格来解决此问题:

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

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

X 是由非限定查找(3.4.1)产生的查找集,并且 让Y 成为由参数相关查找产生的查找集 (定义如下)。如果X 包含

  • 类成员的声明,或
  • 不是using-declaration的块范围函数声明,或
  • 既不是函数也不是函数模板的声明

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

因此,如果提供了using-声明,则通过非限定名称查找找到的唯一声明将是引入model::draw 声明区域的全局draw 模板。 然后调用 ADL 并为 MyClass const&amp; 找到后来声明的 draw 函数。

【讨论】:

  • @SebastianKramer 这就是所谓的“两阶段查找”。考虑void foo(void *); template&lt;class T&gt; void bar(T /*dummy*/){ foo(0); } void foo(int); int main(){ bar(0); } 大概模板的作者打算bar 调用void * 重载而不是int 重载。
  • @T.C.您确定它与 using 声明一起使用的事实符合标准吗?我有点担心 [namespace.udecl]/11
  • @SebastianKramer 第一个项目符号的理由:这是确保在模板定义之后不搜索不依赖于模板参数的名称。因为非依赖名称的名称查找应与通常的函数一样。还要考虑 [temp.nondep]:“模板定义中使用的非依赖名称是使用通常的名称查找找到的,并在使用它们时绑定。”
  • @Columbo 但是你不依赖using 来引入MyClass &amp; 的过载。你依赖于 ADL。
  • 此外,如果您将虚拟方法重命名为 like draw_impl,则对 draw 的非限定调用也可以正常工作。至少在 gcc 上。
【解决方案2】:

当您直接调用::draw() 时,您无法正确使用ADL。 (为什么?我实际上并不知道具体,希望有人也能进来向我解释一下[编辑:请参阅Columbo's answer 的原因])但是为了实际使用 ADL,你需要像这样对draw 进行无条件调用:

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

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

【讨论】:

  • 扩展搜索范围有帮助,谢谢。然而,我正在寻找这种行为背后的理性。
  • @SebastianKramer 好吧,ADL 不能在定义上下文中应用,因为我们不知道那里有什么参数或候选函数。并且希望 ADL 确实适用于实例化上下文(出于与ADL is desirable outside of templates 相同的原因)。这让我们保持现状。
  • 虽然我还不明白为什么不应该在依赖函数调用的实例化上下文中应用 3.4.1 查找。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-05
相关资源
最近更新 更多