4

The code:

#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;
}

The error:

<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)

I understand what the error is about, but I do not understand why it occurs. HashType64 is only convertible to uint64_t and not to any other integer types, the other overloads are not applicable, how can it be ambiguous?

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

Violet Giraffe
  • 32,368
  • 48
  • 194
  • 335
  • Why is only one of them valid? The usual arithmetic conversions make all the user defined conversions viable. And all the conversion sequences have "user-defined" rank. They are equivalent. – StoryTeller - Unslander Monica Jul 18 '23 at 09:05
  • 5
    Overload resolution happens before checking if the target functions are deleted https://en.cppreference.com/w/cpp/language/function#Deleted_functions – Mat Jul 18 '23 at 09:05
  • 2
    https://stackoverflow.com/questions/14085620/why-do-c11-deleted-functions-participate-in-overload-resolution – Mat Jul 18 '23 at 09:07
  • 4
    `= delete` means that using that function/operator is an error, not that the compiler should use another function instead. Compare to `void f(int); void f(double) = delete;` which means that `f(1.0)` should fail, not use the `int` overload. – BoP Jul 18 '23 at 09:09
  • @Mat: I see, thank you! The explanation in the answer you linked makes sense. So my deleted overloads is exactly what creates the ambiguity. – Violet Giraffe Jul 18 '23 at 09:20
  • You can get around the ambiguity: `return HashType64(0).operator uint64_t() & 0xFFULL;` – Eljay Jul 18 '23 at 11:22
  • @Eljay: that is an interesting suggestion, but wouldn't `static_cast` look simpler, cleaner and easier to comprehend? – Violet Giraffe Jul 18 '23 at 12:43
  • Six of one; half-dozen of the other. – Eljay Jul 18 '23 at 12:43
  • 1
    @VioletGiraffe You can declare the other conversions as `template operator T() const noexcept = delete` if you want to remove the ambiguity. – Mestkon Jul 18 '23 at 13:45
  • @Mestkon: that's amazing! Please make it an answer. – Violet Giraffe Jul 18 '23 at 14:48

2 Answers2

4

When doing overload resolution for & there are candidates for the built-in & operator.

More specifically there is one overload with the signature operator&(L, R) for each promoted integer type L and and each promoted integer type R (not necessarily the same).

Therefore, even with no conversion on the right-hand side there are at least candidates

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 has, on your system according to the error message, conversion operators to int (int32_t), unsigned int (uint32_t), long (int64_t), unsigned long (uint64_t).

That these overloads are defined as deleted doesn't affect overload resolution and so each of the four upper overloads are viable. None of them requires more than a single user-defined conversion without another conversion following it for the first argument and so they are all equally viable. (Even if there were standard conversions following the user-defined conversion, conversion sequences involving different user-defined conversions, e.g. conversion functions, are always considered ambiguous.) Nothing else differentiates them in overload resolution and so it is ambiguous.

If you don't want an overload to participate in overload resolution, then you can't declare it at all. The purpose of = delete is to still have the function be considered in overload resolution as usual, but have overload resolution fail if the deleted overload would be chosen. It is useful to disallow certain arguments from being used that would otherwise choose a non-deleted overload with conversions/reference binding that is unintended.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • I can't find where does it say in the standard that multiple overloads are considered for each `operator&` call. I looked here https://eel.is/c++draft/expr.bit.and#:operator,bitwise_AND and here https://eel.is/c++draft/expr.arith.conv and here https://eel.is/c++draft/conv.prom, clearly it's defined elsewhere, any idea? – Violet Giraffe Jul 18 '23 at 15:20
  • 1
    @VioletGiraffe https://eel.is/c++draft/over.built. [expr] only describes the behavior of the built-in operators _after_ they have been chosen by overload resolution as specified in [over]. See explanation in [expr.pre]. – user17732522 Jul 18 '23 at 15:21
  • Thank you. But I don't understand where or why the promotions occur. This is very unintuitive and seems wrong. Do you have any idea why the built-in operators are defined this way in the standard? – Violet Giraffe Jul 18 '23 at 15:27
  • @VioletGiraffe "promoted" in that paragraph just means that integer types with rank below `int`, e.g. `char` and `short`, are excluded from the overload list. If a built-in arithmetic operator would be chosen with such a type, then they would be promoted to `int` (or `unsigned int`) anyway after the user-defined conversion (that's the _usual arithmetic conversions_ that are applied to all built-in arithmetic operators), so that wouldn't be helpful. – user17732522 Jul 18 '23 at 15:31
  • Okay, but my code only uses integers that rank above `int`, specifically - `unsigned long long`. Where does it say that the overload resolution will create overloads that cast to a smaller type? – Violet Giraffe Jul 18 '23 at 15:33
  • @VioletGiraffe You need to define somehow what the built-in operators behave like when doing overload resolution in order to decide whether a `&` should be the built-in one or a user-defined overload. And you also need it to decide which conversion function should be used. What other form of the overloads for the built-in `&` (which can be applied to any pair of integer types) would you suggest? – user17732522 Jul 18 '23 at 15:33
  • @VioletGiraffe https://eel.is/c++draft/over.built#17 says that there are such overloads. I am not sure why/how the contents of your class should affect that. – user17732522 Jul 18 '23 at 15:34
  • Where does it say that promotion can convert `long long` to `int`? I'm just not seeing it. The rank of `long long` > the rank of `long` > the rank of `int`. Conversion from `long long` to `int` shouldn't be done and thus shouldn't be considered, no such overload should be created. – Violet Giraffe Jul 18 '23 at 15:39
  • @VioletGiraffe Promotion is the other way around, e.g. `short` -> `int`. I (and the standard) are only saying that because `short` can be promoted to `int`, there isn't an overload `operator&(short, unsigned long long)` in the list of overloads for built-in `&`. It isn't connected to your use of `long long`. – user17732522 Jul 18 '23 at 15:42
  • Alright, thanks for the detailed replies, I give up. You and the standard completely lost me. I think this behavior is wrong, but I can't even parse what the standard is saying, especially since the relevant info is helpfully scattered all over the document. I hate Rust and other "C++ killers", but I'm starting to understand why they gain popularity. – Violet Giraffe Jul 18 '23 at 15:45
  • 1
    @VioletGiraffe You are in a corner of the language here where the compilers don't even agree and the standard itself might be defective (see the CWG issues I linked under the other answer). It isn't really worthwhile discussing the nuances of which behavior is correct, practically speaking. If you explain in your question, what the goal of the deleted conversion operators is, i.e. what behavior exactly you want to achieve, maybe I can direct you in a more straight-forward direction. – user17732522 Jul 18 '23 at 15:56
  • Yeah, I think the compiler writers also didn't piece together a coherent mental model of what it is the standard actually demands, or they did but their models don't agree with each other. The goal, I should hope, is more or less self-explanatory - to only allow using the hash type in 64-bit-wide calculations and catch coding errors where it is truncated to 32 bits. To that end, the `template` suggestion would be perfect, if only it worked with all three compilers. – Violet Giraffe Jul 18 '23 at 16:00
  • 1
    @VioletGiraffe The straight-forward, although more elaborate, path would in that case be to, instead of providing conversion functions, overload the arithmetic operators for your class accordingly, i.e. with deleted overloads for the argument types that shouldn't compile and defined for the ones that should. With C++20 `requires` or SFINAE before C++20 you can save on the amount of overloads for each operator by using a single template for each instead. – user17732522 Jul 18 '23 at 16:08
2

Turning my comment into an answer.

@user17732522 explained in their answer why the code in the question doesn't work.

An alternative to the code in the question is to declare the deleted operators as a template.

template<class T>
operator T() const noexcept = delete;

This works because template conversion operators does not participate in overload resolution.

The template will still deny any assignment from the class type to anything other than std::uint64_t

std::uint64_t val = HashType64(0); // Works fine
// int val = HashType64(0); // Does not compile
Mestkon
  • 3,532
  • 7
  • 18
  • I don't see why this should change anything. Without looking into the standard, it seems to me that GCC and MSVC are wrong to accept it. Clang actually still provides the same error message about ambiguity as I would expect. – user17732522 Jul 18 '23 at 15:00
  • In particular "_template conversion operators does not participate in overload resolution_" does not seem correct to me. Where does this exception come from? – user17732522 Jul 18 '23 at 15:02
  • @user17732522: seems right to me because a template function only exists after it is instantiated. A conversion operator is instantiated by an explicit or implicit conversion. Who would instantiate it with anything other than the allowed `uint64_t` in my code? I don't expect the built-in `operator&` to suddenly create a bunch (more than 1) instantiations. – Violet Giraffe Jul 18 '23 at 15:04
  • @VioletGiraffe Instantiation itself is not relevant here. That happens only after an overload has been chosen. What you mean is that the template specializations need to be considered for multiple types via template argument deduction. – user17732522 Jul 18 '23 at 15:08
  • @VioletGiraffe But that is what's actually happening. As I mentioned in my answer there is one individual _non-template_ `operator&` overload for the built-in `&` for each pair of promoted integer types. Against each of these overload's first parameters template argument deduction for the conversion function template will be done, resulting in the matching specialization of the conversion function template, with which that overload of the built-in `operator&` is then viable, and all equally well. – user17732522 Jul 18 '23 at 15:08
  • 1
    This is actually [CWG issue 954](https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#954) and [CWG issue 545](https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#545), both of which are currently unresolved. – user17732522 Jul 18 '23 at 15:54