5
template<typename Integral>
struct IntegralWrapper {
    Integral _value;

    IntegralWrapper() = default;

    IntegralWrapper(Integral value)
        : _value(value) {}
    
    operator Integral() const {
        return _value;
    }

    operator bool() const = delete;
};

int main() {
    IntegralWrapper<int> i1, i2;
    i1 * i2;
}

It's compiled successfully by gcc, but failed by MSVC and clang, with error overloaded operator '*' is ambiguous. The problem comes from the explicit deleted operator bool.

https://godbolt.org/z/nh6M11d98


Which side (gcc or clang/MSVC) is right? And why?

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
zjyhjqs
  • 609
  • 4
  • 11

1 Answers1

6

First of all: Deleting a function does not prevent it from being considered in overload resolution (with some minor exceptions not relevant here). The only effect of = delete is that the program will be ill-formed if the conversion function is chosen by overload resolution.


For the overload resolution:

There are candidate built-in overloads for the * operator for all pairs of promoted arithmetic types.

So, instead of using * we could also consider

auto mul(int a, int b) { return a*b; } // (1)
auto mul(long a, long b) { return a*b; } // (2)
// further overloads, also with non-matching parameter types

mul(i1, i2);

Notably there are no overloads including bool, since bool is promoted to int.

For (1) the chosen conversion function for both arguments is operator int() const instantiated from operator Integral() const since conversion from int to int is better than bool to int. (Or at least that seems to be the intent, see e.g. https://github.com/cplusplus/draft/issues/2288 and In overload resolution, does selection of a function that uses the ambiguous conversion sequence necessarily result in the call being ill-formed?).

For (2) however, neither conversion from int or bool to long is better than the other. As a result the implicit conversion sequences will for the purpose of overload resolution be the ambiguous conversion sequence. This conversion sequence is considered distinct from all other user-defined conversion sequences.

When then comparing which of the overloads is the better one, neither can be considered better than the other, because both use user-defined conversion sequences for both parameters, but the used conversion sequences are not comparable.

As a result overload resolution should fail. If I completed the list of built-in operator overloads I started above, nothing would change. The same logic applies to all of them.

So MSVC and Clang are correct to reject and GCC is wrong to accept. Interestingly with the explicit example of functions I gave above GCC does reject as expected.


To disallow implicit conversions to bool you could use a constrained conversion function template, which will not allow for another standard conversion sequence after the user-defined conversion:

template<std::same_as<int> T>
operator T() const { return _value; } 

This will allow only conversions to int. If you can't use C++20, you will need to replace the concept with SFINAE via std::enable_if.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • Perhaps you could add the [`explicit operator bool() const = delete;`](https://godbolt.org/z/8r1bcx8a4) version to make it work as OP wants as bonus? – Ted Lyngmo Mar 26 '22 at 18:08
  • @TedLyngmo I am not sure whether that is the correct solution to their underlying problem. It would prevent explicit conversions to `bool` but not implicit ones. Usually one wants it the other way around. Maybe OP can provide some context on why they need to delete the operator. – user17732522 Mar 26 '22 at 18:10
  • @user17732522 It seems worked. https://godbolt.org/z/x3P6rvdn9 – zjyhjqs Mar 27 '22 at 03:25
  • 1
    @zjyhjqs The condition of an `if` is contextually converted, which is effectively explicit conversion. What I meant is implicit conversion like this: https://godbolt.org/z/er9PMzWex The call is not prevented and will be ambiguous if you add e.g. a `f(long)` overload. The question is whether you want to prevent the explicit or the implicit conversions or both. – user17732522 Mar 27 '22 at 03:31
  • In your case, the whole resolution failed before termination due to the ambiguity in `(2)`? – zjyhjqs Mar 27 '22 at 09:40
  • @zjyhjqs Are you referring to my answer or my comment? In the answer the implicit conversion sequence for `(2)` is ambiguous, but that itself is not an issue. It would be an issue if the function with ambiguous conversion sequence was selected by overload resolution, but here the issue is that an ambiguous sequence is not considered better or worse than any other user-defined conversion sequence, so overload resolution can't select either of the two functions. – user17732522 Mar 27 '22 at 13:49
  • @user17732522 But how does the deleted `operator bool` effect on it. Isn't that the deleted function has been opted by overload resolution, and then found that it's ill-performed when comparing causes the failure in overload resolution? – zjyhjqs Mar 27 '22 at 14:02
  • @zjyhjqs `= delete` is completely irrelevant. If you had just declared or defined `operator bool` in any other way the problem would be the same: Neither of the two conversion operators is better than the other for `(2)` which makes an ambiguous conversion. An ambiguous conversion is neither better nor worse than the `operator int` conversion in `(1)`, so overload resolution cannot choose between `(1)` and `(2)`. – user17732522 Mar 27 '22 at 14:06
  • @user17732522 The explicitly declared `operator bool` (not declared as explicit) causes a direct implicit conversion from `wrapper` to `bool` has been found, and then the `(2)` is selected in overload resolution (which shouldn't be opted) ? – zjyhjqs Mar 27 '22 at 14:18
  • 1
    @zjyhjqs `(2)` is a viable candidate even without `operator bool`. `operator int` can also convert the wrapper to `int` and then to `long`. However if that was the only conversion, the conversion in `(1)` would be considered better than in `(2)`, because it doesn't require a second conversion step. The additional `operator bool` just makes it so that the conversion sequences required in `(2)` are not considered anymore worse than those required for `(1)`. For all the detailed rules see https://en.cppreference.com/w/cpp/language/overload_resolution. – user17732522 Mar 27 '22 at 14:28