为什么当只有一个重载有效时,运算符&会产生歧义?

4

代码:

#include <stdint.h>

struct HashType64
{
    inline HashType64(uint64_t h) noexcept : _h(h) {}
    inline operator uint64_t() const noexcept { return _h; }
    inline operator int64_t() const = delete;
    inline operator int32_t() const = delete;
    inline operator uint32_t() const = delete;

private:
    uint64_t _h;
};

uint64_t f() {
    return HashType64(0) & 0xFFULL;
}

错误:
<source>: In function 'uint64_t f()':
<source>:17:26: error: ambiguous overload for 'operator&' (operand types are 'HashType64' and 'long long unsigned int')
   17 |     return HashType64(0) & 0xFFULL;
      |            ~~~~~~~~~~~~~ ^ ~~~~~~~
      |            |               |
      |            HashType64      long long unsigned int
<source>:17:26: note: candidate: 'operator&(uint32_t {aka unsigned int}, long long unsigned int)' (built-in)
   17 |     return HashType64(0) & 0xFFULL;
      |            ~~~~~~~~~~~~~~^~~~~~~~~
<source>:17:26: note: candidate: 'operator&(int32_t {aka int}, long long unsigned int)' (built-in)
<source>:17:26: note: candidate: 'operator&(int64_t {aka long int}, long long unsigned int)' (built-in)
<source>:17:26: note: candidate: 'operator&(uint64_t {aka long unsigned int}, long long unsigned int)' (built-in)

我明白错误的原因,但不明白为什么会发生这个错误。`HashType64`只能转换为`uint64_t`类型,而不能转换为其他整数类型,其他重载函数都不适用,为什么会出现歧义呢?

https://godbolt.org/z/5zf1MPY4Y


5
在检查目标函数是否被删除之前,会发生重载解析。请参考:https://en.cppreference.com/w/cpp/language/function#Deleted_functions - Mat
5
在检查目标函数是否被删除之前,会发生过载解析。请参考:https://en.cppreference.com/w/cpp/language/function#Deleted_functions - Mat
4
= delete 的意思是使用该函数/运算符是错误的,而不是编译器应该使用另一个函数。与 void f(int); void f(double) = delete; 相比,这意味着 f(1.0) 应该失败,而不是使用 int 重载。 - BoP
4
= delete 的意思是使用该函数/运算符是错误的,而不是编译器应该使用另一个函数。与 void f(int); void f(double) = delete; 相比,这意味着 f(1.0) 应该失败,而不是使用 int 重载。 - BoP
4
= delete 的意思是使用该函数/运算符是错误的,而不是编译器应该使用另一个函数。与 void f(int); void f(double) = delete; 相比,这意味着 f(1.0) 应该失败,而不是使用 int 重载。 - undefined
显示剩余19条评论
2个回答

4

在进行&的重载解析时,存在内置&运算符的候选项。

更具体地说,对于每个提升的整数类型L和每个提升的整数类型R(不一定相同),都有一个带有签名operator&(L, R)的重载。

因此,即使在右侧没有转换的情况下,至少存在候选项。

operator&(int,                unsigned long long)
operator&(unsigned,           unsigned long long)
operator&(long,               unsigned long long)
operator&(unsigned long,      unsigned long long)
operator&(long long,          unsigned long long)
operator&(unsigned long long, unsigned long long)

根据错误信息,您的系统上的HashType64具有到intint32_t)、unsigned intuint32_t)、longint64_t)、unsigned longuint64_t)的转换运算符。
尽管这些重载被定义为delete,但它们不会影响重载决议,因此这四个重载都是可行的。对于第一个参数,它们都不需要多于一个用户定义的转换,并且它们的可行性相同。即使存在标准转换跟随用户定义的转换,涉及不同用户定义转换的转换序列(例如,转换函数)始终被视为模棱两可。在重载决议中没有其他区别,因此它是模棱两可的。
如果您不希望某个重载参与重载决议,那么您根本不能声明它。= delete的目的是使该函数像往常一样参与重载决议,但如果选择了已删除的重载,则重载决议将失败。这对于禁止使用某些参数非预期地选择非删除的重载并进行转换/引用绑定非常有用。

1
@VioletGiraffe https://eel.is/c++draft/over.built. [expr] 只描述了在[over]中规定的重载解析选择内置运算符之后的行为。请参阅[expr.pre]中的解释。 - user17732522
1
@VioletGiraffe https://eel.is/c++draft/over.built. [expr] 只描述了在[over]中指定的重载解析选择内置运算符之后的行为。请参阅[expr.pre]中的解释。 - user17732522
1
@VioletGiraffe 在这个领域里,编译器甚至都不能达成一致,而标准本身可能存在缺陷(请参阅我在另一个回答中提供的CWG问题链接)。从实际角度来讲,讨论正确行为的细微差别并不值得。如果你在问题中解释清楚被删除的转换操作符的目标,也就是你想要实现的确切行为,也许我能够为你指明更直接的方向。 - user17732522
1
@VioletGiraffe 如果是这样的话,直接而详细的方法将是为您的类重载相应的算术运算符,而不是提供转换函数。也就是说,对于不应该编译的参数类型,使用删除的重载,对于应该编译的参数类型,使用定义的重载。在C++20中,可以使用requires或者在C++20之前使用SFINAE来减少每个运算符的重载数量,通过使用单个模板来实现。 - user17732522
1
@VioletGiraffe 如果是这样的话,直接而更加详细的方法将是为您的类重载相应的算术运算符,而不是提供转换函数。也就是说,对于不应该编译的参数类型,使用删除的重载,对于应该编译的参数类型,使用定义的重载。在C++20中,您可以使用requires或在C++20之前使用SFINAE来减少每个运算符的重载数量,通过为每个运算符使用单个模板。 - user17732522
显示剩余28条评论

2
将我的评论转化为一个答案。
@user17732522在他们的答案中解释了问题中的代码为什么不起作用。
问题中代码的另一种替代方法是将删除的运算符声明为模板。
template<class T>
operator T() const noexcept = delete;

这是因为模板转换运算符不参与重载决议,所以它能够正常工作。
模板仍然会拒绝将类类型赋值给除了std::uint64_t之外的任何其他类型。
std::uint64_t val = HashType64(0); // Works fine
// int val = HashType64(0); // Does not compile

特别是"template转换运算符不参与重载决议"对我来说似乎不正确。这个例外是从哪里来的? - user17732522
@user17732522:对我来说似乎是正确的,因为模板函数只有在实例化之后才存在。转换运算符通过显式或隐式转换进行实例化。除了允许的uint64_t之外,谁会用其他东西来实例化它呢?我不希望内置的operator&突然创建大量(超过1个)的实例。 - Violet Giraffe
@VioletGiraffe 但这实际上就是正在发生的事情。正如我在回答中提到的,对于每一对提升的整数类型,内置的&操作符都有一个非模板的operator&重载。针对每个重载的第一个参数,将进行模板参数推断以获得转换函数模板的匹配特化结果,然后该内置operator&的重载将成为可行且同样有效的选项。 - user17732522
1
这实际上是CWG问题954CWG问题545,目前都没有解决。 - user17732522
1
这实际上是CWG问题954CWG问题545,目前两个问题都没有解决。 - user17732522
显示剩余8条评论

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