C++运算符重载及关联命名空间

5
下面这个简化的例子可以在gcc和Visual Studio中编译通过,但是在clang中却失败了!?
namespace N
{
    struct A {};

    template <typename T>
    double operator+ (T a, double d) {return d;}

    template <typename T>
    double operator+ (double d, T a) {return d;}
}

void test()
{
    N::A a;
    double x;

    double y = a + x;
    double z = x + a;
}

依我看来,名称空间N中的模板化operator+应该由ADL(通过参数相关性查找)得到。

为什么clang不同意?这是clang的一个bug还是其他编译器的问题?

以下是来自clang 3.5.1的编译错误信息(在coliru上测试),我不明白出了什么问题...

10 : error: overloaded 'operator+' must have at least one parameter of class or enumeration type
double operator+ (double d, T a) {return d;}
^
18 : note: in instantiation of function template specialization 'N::operator+' requested here
double y = a + x;
^

7 : error: overloaded 'operator+' must have at least one parameter of class or enumeration type
double operator+ (T a, double d) {return d;}
^
19 : note: in instantiation of function template specialization 'N::operator+' requested here
double z = x + a;
^

2 errors generated.
Compilation failed

这个例子是从真实代码简化而来。意图是在命名空间N中定义的任何类都有一个重载的operator+方法,可以与double相加。


从那个错误来看,似乎是被ADL找到了。不过不确定它在抱怨什么。 - xcvr
请注意,您的测试程序存在未定义的行为,因为x未初始化。 - Kerrek SB
@KerrekSB,你正在复制,因为你正在使用gcc编译,而clang无法编译。这里是证明。 - David G
2个回答

5
这是由于两个不同的CWG问题引起的:CWG问题2052CWG问题1391
首先,是CWG 1391。当遇到x + a时,通常的名称查找会发现,在其他重载函数中:
template <typename T> double operator+ (T, double);

模板参数推断是通过匹配+左侧的类型double来匹配T的类型,所以它会将T推断为double。第二个参数的类型不包含任何模板参数,因此在当前规则下不被考虑。确切地说,N::A不能转换为double,因此生成的特化不可行,但是目前的规则说模板参数推断不关心这个问题;这将在重载决定中处理。
针对CWG 1391提出的解决方案增加了标准的新段落,其中包括以下内容:
如果所有包含参与模板参数推断的模板参数的参数都推导成功,并且所有模板参数都是显式指定、推导或从默认模板参数中获取的,则将剩余的参数与相应的参数进行比较。对于每个剩余的参数P,在任何显式指定模板参数替换之前它的类型是非依赖性的情况下,如果相应的参数A无法隐式转换为P,则推断失败。[注:具有依赖类型的带有模板参数的参数和由于显式指定模板参数替换而变为非依赖性的参数将在重载决议期间进行检查。结束注释]
换句话说,如果对于一个非依赖性参数(double)的相应参数(a)无法转换为该参数的类型,则推断将简单地失败。因此在我们的情况下,基于CWG1391后的模板参数推断将针对此重载失败,一切都会很好。
然而,Clang实现了当前的规则,因此推断成功,并进行了替换,然后我们遇到了CWG 2052。引用Clang开发人员Richard Smith的写作:

In an example like

  struct A { operator int(); };
  template<typename T> T operator<<(T, int);
  void f(A a) { 1 << a; }

Template argument deduction succeeds for the operator template, producing the signature operator<<(int,int). The resulting declaration is synthesized and added to the overload set, per 14.8.3 [temp.over] paragraph 1. However, this violates the requirement of 13.5 [over.oper] paragraph 6,

An operator function shall either be a non-static member function or be a non-member function that has at least one parameter whose type is a class, a reference to a class, an enumeration, or a reference to an enumeration.

This is not a SFINAE context, so the program is ill-formed, rather than selecting the built-in operator.

在这种情况下,没有转换,因此推断出的operator+(double, double)实际上不可行,但是在构建候选集之前不会消除不可行的候选项,而在这里构建候选集会导致严重错误。
对于CWG 2052提出的解决方案将使这种情况成为SFINAE - 同样使原始代码起作用。问题是- Clang也在此处实现了当前版本的标准。

在 OP 的情况下,为什么会推导出 operator+(double, double) - Kerrek SB
@KerrekSB 给定 template <typename T> double operator+ (T, double); double x; N::A a;x + aT 从左侧被推导出来,即为 double。当然,对于右侧的 N::Adouble 的转换是不可能的,但模板参数推导并不关心这一点;这留给了重载决议。 - T.C.
标准中有一个有趣的漏洞。感谢您的解释。我猜clang是正确的,GCC/VC也应该是对的。 - xcvr
@T.C.:哦,当然——我想到了另一个函数。是的,很有道理。这本应该是SFINAE上下文,但不是。我想CWG问题将会成为针对11的DR —— 没有人希望这种行为取决于语言版本。 - Kerrek SB
感谢您的解释。根据这个答案,我只有两个选择:a)为命名空间N中的每个类提供重载运算符,或者b)在命名空间N中的所有类中使用CRTP,然后基于共同的基类定义运算符。我是对的吗? - dats
@dats 不,另一个答案中的 enable_if 是可以的。当 T 推导为 double 时,它会导致一次推导失败,因此替换(形成不良形式的 operator+(double, double))就不会发生。 - T.C.

3

可能是因为在那个定义中 T 可能不是 一个类,所以它在抱怨。另外,据我所知,您不允许重新定义算术类型的标准 operator+。在您的示例中,没有任何限制使得 T 只能是例如 N::A

添加 typename = std::enable_if_t<std::is_class<T>{} || std::is_enum<T>{}> 似乎可以解决这个问题。Visual Studio 和 GCC 对这个限制可能会更加宽松/懒惰。

namespace N
{
    struct A {};

    template <typename T, typename = std::enable_if_t<std::is_class<T>{} || std::is_enum<T>{}>>
    double operator+ (T a, double d) {return d;}

    template <typename T, typename = std::enable_if_t<std::is_class<T>{} || std::is_enum<T>{}>>
    double operator+ (double d, T a) {return d;}
}

 void test()
 {
    N::A a;
    double x;

    double y = a + x;
    double z = x + a;
 }

我认为在实例化之前,这个问题不应该成为一个关注点。例如,如果您删除其中一个重载,clang就不会抱怨。 - David G
@0x499602D2 真的。这里发生了一些奇怪的事情。如果您保留另一个加法,http://coliru.stacked-crooked.com/a/c3dd251ee27581b3,仍然会弹出该错误。 - xcvr
错误信息(原始的和 coliru 的)指出在编译失败的实例中,它正在使用 T = double 实例化函数。在此处请求“N::operator+<double>”函数模板的特定实例化 - xcvr
是的,我认为clang在执行重载决议时无意中实例化了候选项。 - David G
根据T.C的回答,我不认为enable_if能够解决clang中的问题。 - dats
@dats 它肯定修复了这个问题。http://coliru.stacked-crooked.com/a/f82b2eeed3600729 - xcvr

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