模板运算符重载解析,成员函数与非成员函数

9
尝试使用从git编译的clang-3.4时,它无法编译我的一个项目,并抱怨在解决重载运算符时存在歧义。原来有两个模板运算符,其中一个被声明为成员函数,另一个被声明为非成员函数,两者看起来都是同样好的匹配。
下面的SSCCE演示了这种情况:
#include <iostream>

struct ostr {
        std::ostream& s;

        template<class T>
        ostr& operator<<(const T& x) { s << x; return *this; }
};

struct xy {
        double x, y;
};

template<class Stream>
Stream& operator<<(Stream& s, const xy& x) {
        s << "[" << x.x << ", " << x.y << "]";
        return s;
}

int main() {
        ostr os{std::cout};
        xy x{4, 5};
        os << "Value is: " << x <<"\n";
}

在添加了`ctor`到`ostr`之前,该项目编译正常。我使用了几个编译器(`gcc 4.5`,`4.6`,`4.7`,`4.8`和`clang 3.3`)对这个SSCCE进行了检查,并且它们都可以在没有任何警告的情况下进行编译(使用`-Wall -Wextra -pedantic`)。所有编译器都设置为C++11/C++0x标准。 在`ostr`中添加`ctor`后,即使在`MSVC 2012`和`2010`上也可以编译成功。
将两个`operator<<`都变成非成员函数会在所有编译器中表现出模棱两可的结果(如预期所示)。
在查看标准草案(`N3242`和`N3690`)之后,我未能找到任何使成员函数/运算符优于非成员函数/运算符的内容。
因此,我无法证明`clang-3.4`是错误的,我想知道谁是正确的。 因此,我的问题是:
  • 这段代码有效吗?成员运算符/函数应该比非成员运算符/函数更匹配,这是`clang-3.4`的一个错误吗?
  • 或者所有其他编译器都是错的/太宽容了?
我知道将第二个`operator<<`更改为非模板函数(使用`std::ostream`而不是模板参数)将解决歧义并按预期工作,但这不是这里的重点。

2
+1 因为这是一个有趣的问题;但如果可以的话,我会再次 +1 您的清晰度和彻底性! - Chowlett
成员运算符/函数是否比非成员更匹配? 不是,但它们会得到一个额外的参数,即对其类类型的引用,以便进行重载决议。 - dyp
DyP:那个错误据说在r190470中已经修复。我用当前版本(r194576,刚刚提交一个小时前)进行了测试,结果仍然相同。 - v154c1
尝试使用r190470之前的版本,例如在coliru中使用的184460版本:它的行为类似于其他编译器。 - dyp
DyP:你说得对,r190470 是改变的地方。我刚试了一下 190646(使用 git 镜像能找到的最接近的提交),它可以正常工作。而 r190470 拒绝了当前版本拒绝的代码。 - v154c1
显示剩余2条评论
1个回答

4

重载决议(Overload resolution)在成员函数中添加了一个额外的参数,仅用于重载决议:

[over.match.funcs]/2

候选函数集合可以包含成员函数和非成员函数,以便对相同的参数列表进行解析。为了使这个异构集合中的参数列表和形参列表可比较,成员函数被认为有一个额外的参数,称为“隐式对象参数”,它表示调用成员函数的对象。

/4

对于非静态成员函数,隐式对象参数的类型为:

— 对于没有 ref-qualifier 或使用 & ref-qualifier 声明的函数,“lvalue reference to cv X

— 对于使用 && ref-qualifier 声明的函数,“rvalue reference to cv X

其中,X 是该函数所属的类,cv 是成员函数声明上的 cv-qualification。

还有一些特殊规则需要遵循,例如允许将 rvalue 绑定到这个隐式对象参数(用于在 rvalues 上调用没有 ref-qualifier 的成员函数,例如 ostr{std::cout}<<"hello")。


我们需要比较的用于重载决议的函数签名包括隐式对象参数:

template<class T>
ostr& ostr::operator<<(ostr&, const T&);    // F1

template<class Stream>
Stream& ::operator<<(Stream&, const xy&);    // F2

替换os << x后,我们得到了相同的签名:
ostr& ostr::operator<<(ostr&, const xy&);
ostr& ::    operator<<(ostr&, const xy&);

我只能从[over.match.best]/1的“tie-breakers”中选择一个来解决歧义。确实,可以应用其中一个,即“F1F2更专业”(或反之):函数模板的部分排序。

注意,在部分排序[temp.func.order]/3的描述中再次指定了添加隐式对象参数的过程。


对于F1F2的部分排序(如上所定义),我们首先创建两个独特的类型:

struct unique_T {};
struct unique_Stream {};

我们将F1转换为F1',通过用唯一类型unique_T替换模板参数T(对于F2同样如此):

ostr& ostr::operator<<(ostr&, const unique_T&);
ostr& ::    operator<<(unique_Stream&, const xy&);

转换后的函数 F1' 的参数现在被用来尝试推导未转换的函数 F2 的模板参数。
ostr a0;
unique_T a1; // no reference, no cv-qualifier
::operator<<(a0, a1) // does template argument deduction succeed?

// reminder: signature of ::operator<<
template<class Stream>
Stream& ::operator<<(Stream&, const xy&);

这个推断成功适用于a0(使用Stream=ostr),因此来自F1的类型ostr&被认为至少与F2的相应第一个参数的类型(Stream&,其中Stream是模板参数)一样专业化。我不确定第二个参数a1会发生什么情况,因为对于::operator<<的第二个参数(它的类型为const xy&),没有进行推断。

现在我们使用F2'中的参数重复这个过程,并尝试推断F1的模板参数:

unique_Stream a0;
xy a1;
ostr::operator<<(a0, a1);

// reminder: signature of ostr::operator<<
template<class T>
ostr& ostr::operator<<(ostr&, const T&);

在这里,第一个参数不会发生推断,但是第二个参数会发生并成功推断 [其中 T = xy]。

我得出结论,没有任何函数模板更加特化。因此,由于歧义,重载解析应该失败


我确定吗?不确定。请添加任何提示以帮助澄清问题。 - dyp
非常感谢您提供的逐步指导,您让我对[over.match.best]有了更清晰的认识 :). 因此,该代码应该是无效的,在 r190470 之后的 clang 是唯一正确的编译器?看起来确实是这样的... - v154c1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接