MSVC and constexpr for function parameter?

Viewed 406

This code compile fine with clang and gcc.

template<size_t n>
struct N {
    static constexpr size_t v = n;
};

template<size_t n>
constexpr bool operator<(N<n>, size_t n2) {
    return n < n2;
}

template<typename N>
constexpr void foo(N v) {
    static_assert(v < 5);
}

int main()
{
    foo(N<3>{});
    return 0;
}

However, if I use MSVC, I got the error that v < 5 is not a constant expression. I can understand why MSVC thinks that, but I think it is wrong and clang / gcc are right. Is it a bug from MSVC?

3 Answers

Yes, MSVC is wrong here.

It may seem counter-intuitive that the code is well-formed, because how can v, which is not a constant expression, possibly be used in a constant expression?

So why is it allowed? Well first, consider that informally, an expression is not a constant expression if it evaluates to a glvalue that is itself not a constant expression or a variable that started its life outside of the the enclosing expression ([expr.const]p2.7).

Second, operator< is constexpr.

Now, what happens is that v < 5 is a valid constant expression. To understand that, let's go through the evaluation of the expression.

We have:

  1. v < 5 calls your constexpr operator<
  2. The two parameters are copied (both of them literals and none evaluate to a non-constexpr object)
  3. n2 started its life within the evaluation of v < 5 and is a literal
  4. n is a non-type template parameter, as such usable in a constant expression
  5. Finally, n < n2 invokes a builtin operator.

All of those do not violate any of the points in [expr.const]p2, so the resulting expression is in fact a constant expression that be used as an argument to static_assert.

Those types of expressions are known as converted constant expressions.

Here's a simplified example:

struct Foo {
  constexpr operator bool() { return true; }
};

int main() {
  Foo f;
  static_assert(f);
}

MSVC is incorrect here, let's start with a simplified version of the code:

struct N {
    static constexpr size_t v = 0;
};

constexpr 
  bool operator<(N n1, size_t n2) {
    return n1.v < n2;
}

  void foo(N v) {
    static_assert(v < 5, ""); // C++11 does not allow terse form
}

We will look at the static_assert first, have we violated any rules for constant expressions? If we look at [expr.const]p2.2:

an invocation of a function other than a constexpr constructor for a literal class or a constexpr function [ Note: Overload resolution (13.3) is applied as usual —end note ];

We are good, operator< is constexpr function and the copy constructor for N is constexpr constructor for a literal class.

Moving to operator< and examine the comparison n1.v < n2 and if we look at [expr.const]p2.9:

an lvalue-to-rvalue conversion (4.1) unless it is applied to
- a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression, or
- a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object, or
- a glvalue of literal type that refers to a non-volatile temporary object whose lifetime has not ended, initialized with a constant expression

We are good here as well. In the original example we are referring to a template non-type argument which is usable in a constant expression so the same reasoning applies to that case as well. Both operands of < are usable constant expressions.

We can also see that MSVC still treats the simplified case as ill-formed even though clang and gcc accept it.

If you had declared:

template<size_t n>
struct N {
  int i;
  static constexpr size_t v = n;
 };

Demo here.

Both MSVC, Clang and GCC would reject your code. The reason is that v is copied into the first parameter of operator<. Such a copy is an evaluation of v and v is not a constant expression.

In your case N is an empty class. I don't think the standard specifies if the copy constructor of such a class should access the memory of the object 1 (core language issue 1701). So the compilers showes a behavior that depends on whether or not the memory of object of empty class is accessed.

Clang and GCC, does not access the memory of object of empty classes when passed has parameters, but MSVC does: see this compiler explorer link

So I think all compilers are right.


1 Accessing the memory representation of the object to copy padding bits would involve a reinterpret_cast (or equivalent) which is also forbiden in constant expression.

Related