2

Please, consider the following code:

template<typename T>
T operator+(const T& lvh, const T& rvh)
{
    return lvh;
}

struct enum_container {
    enum {
        item1 = 1,
        item2 = 2,
        item3 = item1 + item2
    };
};

int main() {
    return 0;
}

Compiling with MS VS 2022 gives the following error:

error C2131: expression did not evaluate to a constant
message : failure was caused by call of undefined function or one not declared 'constexpr'
message : see usage of 'operator +'

The problem applies to the third element in enum.
Why cl tries to substitute template for the constant expression?
Compilation with gcc gives no errors.
I can eliminate the error with cl adding type specifier to the enum, like this:

enum : int {
...
}

But I can`t do this in the project I work with. Both enum and overload declared by 3rd party headers. All I can do is to change includ order. But I'm still wondering why it works that way...

Ivan Pankov
  • 178
  • 1
  • 7
  • @SamVarshavchik Thank you. Actually the real code doesn`t looks exactly like this. I made some edits - now operator returns first value... – Ivan Pankov Aug 30 '22 at 17:03
  • 2
    `template T operator+(const T& lvh, const T& rvh)` should be removed from whatever codebase it is in, *immediately*. First-party, second-party, third-party, twenty-ninth-party, just remove it. [Live Demo](https://godbolt.org/z/fnhejKebP) – n. m. could be an AI Aug 30 '22 at 17:13
  • Hmm. Clang-cl compiles your (edited) code as-is. Adding a `constexpr` to the function template keeps MSVC (cl) happy, though. What C++ Standard are you using? – Adrian Mole Aug 30 '22 at 17:25
  • `All I can do is to change includ order.` Doesn't including the overload after the structure help? – The Dreams Wind Aug 30 '22 at 18:08
  • 1
    An unrestricted template like `template T operator+(const T& lvh, const T& rvh)` now defines `+` for every type ever written. This is not just a foot-gun, but a weapon of mass destruction. – BoP Aug 30 '22 at 18:47
  • @TheDreamsWind Yes, it helps. It was the first thing I do. But I want to understand MSVC behavior. Why it tries to look substitute template in enum initialization – Ivan Pankov Aug 30 '22 at 18:56
  • @BoP Yeah I know it :) It isn't mine... – Ivan Pankov Aug 30 '22 at 18:58

1 Answers1

1

To me it looks like MSVC is correct here.

First, because template argument deduction should happen before overload resolution to form a set of candidate functions:

Before overload resolution begins, the functions selected by name lookup and template argument deduction are combined to form the set of candidate functions

And then, since the non-member candidates (in this case template specialisation of the operator+ overload with the enum_container::<anonymous_enum> parameters) precede built-in candidates (conversion to the underlying built-in type, for which a user-defined overload would not be possible), the compiler is meant to resolve the overload to the instance of the template with the user-defined enum-type:

  1. non-member candidates: For the operators where operator overloading permits non-member forms, all declarations found by unqualified name lookup of operator@ in the context of the expression (which may involve ADL), except that member function declarations are ignored and do not prevent the lookup from continuing into the next enclosing scope. If both operands of a binary operator or the only operand of a unary operator has enumeration type, the only functions from the lookup set that become non-member candidates are the ones whose parameter has that enumeration type (or reference to that enumeration type)
  2. built-in candidates: For operator,, the unary operator&, and the operator->, the set of built-in candidates is empty. For other operators built-in candidates are the ones listed in built-in operator pages as long as all operands can be implicitly converted to their parameters. If any built-in candidate has the same parameter list as a non-member candidate that isn't a function template specialization, it is not added to the list of built-in candidates. When the built-in assignment operators are considered, the conversions from their left-hand arguments are restricted: user-defined conversions are not considered.

Thus, the fact that GCC/Clang ignore the existence of the user-defined overload of operator+ under the enum definition doesn't seem to be compliant with the rules listed above. The same expression inside of a block scope causes exactly same error for all compilers (and I couldn't find any rationale why this might be different for the scope of enum definition):

struct enum_container {
    enum {
        item1 = 1,
        item2 = 2,
        item3 = item1 + item2
    };
};

template<typename T>
T operator+(const T& lhs, const T&) {
    return lhs;
}

int main() {
    // error: call to non-'constexpr' function 'T operator+(const T&, const T&)
    constexpr auto enumval = enum_container::item1 + enum_container::item2;
    return 0;
}
The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
  • I think this because GCC associate some integral type to the enum values (even if it was not specified with ": int"). So there is better candidate operator to make sum of ints. And msvc asign something like enum_container:: type - so template substitution happens... – Ivan Pankov Aug 30 '22 at 19:06
  • @IvanPankov i couldn't find a reason why overload resolution for `enumval` and `item3` should be different. GCC knows that the overload exists in both scenarios, and `item1` and `item2` are not built-in types (and even if GCC wanted to convert them, **built-in candidates** should come after **non-member candidates**) – The Dreams Wind Aug 30 '22 at 19:10
  • I added check like ```static_assert(std::is_integral_v, "value is not integral");``` inside operator. With msvc I've got error on this assertion on gcc - no error. I understand that the rule you mantion above is not affected by this fact. But I think this also takes its role... – Ivan Pankov Aug 30 '22 at 19:32
  • @TheDreamsWind, since with the last paragraph you seem to refer to the error happening in `main`, isn't it misleading that you've defined `enum_container` before `operator+`, unlike what's in the question? I mean, the error in `main` is still the same if you keep the orignal order. – Enlico Aug 30 '22 at 19:49
  • @Enlico i made it to make the error uniform between compilers, it wasn't meant to show how to address the error, just to demonstrate that GCC is also aware that such error should happen when the overload comes into play. – The Dreams Wind Aug 30 '22 at 19:50
  • @IvanPankov GCC doesn't complain because it doesn't instantiate the template. `T` cannot be integral for `operator+` as far as i'm aware, it would violate very fundamental rule that you can't overload operators for built-in types. – The Dreams Wind Aug 30 '22 at 19:53