New-expression with consteval constructor in constexpr context
Asked Answered
P

1

35
struct A {       
    consteval A() {};
};

constexpr bool g() {
    auto a = new A;
    delete a;
    return true;
}

int main() {
    static_assert(g());
}

https://godbolt.org/z/jsq35WxKs

GCC and MSVC reject the program, ICC and Clang accept it:

///MSVC: 
<source>(6): error C7595: 'A::A': call to immediate function is not a constant expression
Compiler returned: 2

//GCC:
<source>: In function 'constexpr bool g()':
<source>:6:18: error: the value of '<anonymous>' is not usable in a constant expression
    6 |     auto a = new A;
      |                  ^
<source>:6:18: note: '<anonymous>' was not declared 'constexpr'
<source>:7:12: error: type '<type error>' argument given to 'delete', expected pointer
    7 |     delete a;
      |            ^
Compiler returned: 1

Although, replacing new A by new A() results in GCC accepting the program as well (but not for new A{} either).


Making at least one of the following changes results in all four compilers accepting the program:

  1. Replace consteval with constexpr

  2. Replace constexpr with consteval

  3. Replace

    auto a = new A;
    delete a;
    

    with

    auto alloc = std::allocator<A>{};
    auto a = alloc.allocate(1);
    std::construct_at(a);
    std::destroy_at(a);
    alloc.deallocate(a, 1);
    

    with A a;, with auto&& a = A{}; or with A{};

Only exceptions:

  • Clang trunk with libstdc++ seems to fail compilation with the std::allocator version seemingly due to an unrelated bug. With Clang 13 or libc++ it is accepted as well.

    In file included from <source>:1:
    In file included from [...]/memory:78:
    [...]/shared_ptr_atomic.h:459:14: error: missing 'typename' prior to dependent type name '_Atomic_count::pointer'
      static _Atomic_count::pointer
    
  • MSVC rejects the std::allocator version as long as there is consteval on the constructor:

    error C7595: 'A::A': call to immediate function is not a constant expression
    <source>(10): note: see reference to function template instantiation '_Ty *std::construct_at<_Ty,,void>(_Ty *const ) noexcept(false)' being compiled
            with
            [
                _Ty=A
            ]
    

Replacing static_assert(g()); with g() or removing the call completely does not seem to have any impact on these results.


Which compilers are correct and if the original is ill-formed, why is only that particular combination of qualifiers and construction method disallowed?


Motivated by the comments under this answer.

Polymeric answered 17/1, 2022 at 19:17 Comment(13)
using std::allocator<A> is accepted too DemoAdenitis
The allocator version doesn't actually initialize the object. I reckon a call to construct will behave like the bare new expression.Kun
@StoryTeller-UnslanderMonica Only MSVC has a problem with it. I have added it to the question.Polymeric
Interestingly changing new A to new A() makes GCC happy with the code.Judaic
@Judaic Hm, but not with new A{}. I have updated the question.Polymeric
See #65396585Eolith
@SolomonUcko That bug seems to have been fixed a few versions ago: godbolt.org/z/qcxhvefxvPolymeric
I would have missed this Q&A if I hadn't randomly checked the bountied section :/ You could have pinged me @Polymeric ;)Sibling
@Sibling Oh right. I must have thought that I put out the question shortly enough after my comment that you would see it if you were interested. I will ping next time.Polymeric
I dont read the standard papers to verify my thoughts. But from my understanding: consteval MUST be used in compile-time contexts only. Since constexpr can be used at compile time AND runtime, it will reject consteval expressions. Interestingly, I changed the g function like this: constexpr bool g() { if constexpr( std::is_constant_evaluated() ) { auto a = new A; delete a; } return true; } but the code is still rejected under MSVC 17.3.5 (C++latest).Popham
@TeaAgeSolutions I think this is why, if you use C++23's if consteval then call the consteval constructor in the constexpr function it works, would you like to make the answer?Stamps
@TeaAgeSolutions No, a function call to a consteval function can appear anywhere (explicitly or implicitly), but independently of the context the call must by itself form a constant expression, assuming it does not appear inside another consteval function. The question here is what it means for the implicit constructor call to form a constant expression and how that interacts with the new-expression semantics. Coming back to this question I think the standard isn't specifying this properly, similar to how it doesn't specify the behavior of constexpr variables correctly.Polymeric
@TheFloatingBrain Using if consteval puts the expression in an immediate function context, which is equivalent to just changing the function from constexpr to consteval, which I noted in my question works fine (and this part is not surprising). What is surprising is that new and the other allocation methods produce different behavior with constexpr. See also my comment above.Polymeric
M
5

The relevant wording is [expr.const]/13:

An expression or conversion is an immediate invocation if it is a potentially-evaluated explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.

Note the words 'or conversion' and 'implicit invocation' - this seems to imply that the rule is intended to apply on a per-function-call basis.1 The evaluation of a single atomic expression can consist of multiple such calls, as in the case of e.g. the new-expression which may call an allocation function, a constructor, and a deallocation function. If the selected constructor is consteval, the part of the evaluation of the new-expression that initializes the object (i.e. the constructor call), and only that part, is an immediate invocation. Under this interpretation, using new with a consteval constructor should not be ill-formed regardless of context - even outside of a constant expression - as long as the initialization of the object is itself constant, of course.

There is an issue with this reading, however: the last sentence clearly says that an immediate invocation must be an expression. A 'sub-atomic call' as described above isn't one, it does not have a value category, and could not possibly satisfy the definition of a constant expression ([expr.const]/11):

A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints [...]

A literal interpretation of this wording would preclude any use of a consteval constructor outside of an immediate function context, since a call to it can never appear as a standalone expression. This is clearly not the intended meaning - among other things, it would render parts of the standard library unusable.

A more optimistic (but also less faithful to the words as written) version of this reading is that the atomic expression containing the call (formally: the expression which the call is an immediate subexpression of 2) must be a constant expression. This still doesn't allow your new A construct because it is not a constant expression by itself, and also leaves some uncertainty in cases like initialization of function parameters or variables in general.


I'm inclined to believe that the first reading is the intended one, and that new A should be fine, but clearly there's implementation divergence.

As for the contradictory 'shall be a constant expression' requirement, this isn't the only place in the standard where it appears like this. Earlier in the same section, [expr.const]/2.2:

A variable or temporary object o is constant-initialized if [...]

  • the full-expression of its initialization is a constant expression when interpreted as a constant-expression [...]

Clearly, the following is supposed to be valid:

constinit A a;

But there's no constant expression in sight.


So, to answer your question:

Whether the call to g is being evaluated as part of a manifestly constant-evaluated expression does not matter3 regardless of which interpretation of [expr.const]/13 you go with. new A is either well-formed even during normal evaluation or ill-formed anywhere outside of an immediate function context.

By the looks of it, Clang and ICC implement the former set of rules while GCC and MSVC adhere to the latter. With the exception of GCC accepting new A() as an outlier (which is clearly a bug), neither are wrong, the wording is just defective.


[1] CWG2410 fixes the wording to properly include things like constructor calls (which are neither expressions nor conversions).

[2] Yes, a non-expression can be a subexpression.

[3] Such a requirement would be impossible to enforce.

Maisel answered 5/12, 2022 at 5:29 Comment(4)
"which is clearly a bug": I assume you mean here that it must be a bug because the compiler is being inconsistent with itself, not because new A() is definitively ill-formed, right?Polymeric
As I mentioned in the comments under the question when I was revisiting it I came to a similar conclusion as you. So this makes some sense to me. However I don't really see why GCC consider a placement-new via std::construct_at different from the object construction in the allocating new-expression. That part especially seems inconsistent to me.Polymeric
Yes, I was referring to () making a difference at all in this case. std::construct_at being accepted is a manifestation of the same bug - calling it with no arguments is equivalent to a placement-new that looks like this: new(...) A(). Adding a dummy int parameter causes it to be rejected as well.Maisel
Ah right. GCC might seems to be doing something different for value-initialization specifically.Polymeric

© 2022 - 2024 — McMap. All rights reserved.