1

In C++23, the [[assume(conditonal-expression)]] attribute makes it so that if conditional-expression doesn't evaluate to true, the behavior is undefined. For example:

int div(int x, int y) {
    [[assume(y == 1)]];
    return x / y;
}

This compiles to the same code as if y was always 1.

div(int, int):
        mov     eax, edi
        ret

As commenters have pointed out, this is not a required optimization; it's just what GCC happens to do with the information that anything but y == 1 would be UB.

It would be valid for compilers to completely ignore all assumptions.

But what about constant expressions?

Compilers are required to diagnose all undefined behavior in constant expressions1), but is this reasonable? For example:

constexpr bool extremely_complicated(int x) {
    bool result;
    // 10,000 lines of math ...
    return result;
}

constexpr int div(int x, int y) {
    // This should result in a compiler error when the compiler is unable to prove
    // what extremely_complicated(x) returns.
    // extremely_complicated(x) is not evaluated, so it needs to be able to
    // prove the result without evaluating the function.
    [[assume(extremely_complicated(x))]];
    return x / y;
}

constexpr int quotient = div(4, 2);

Is it still a problem even if the compiler has no way of proving whether an assumption would evaluate to true? It obviously can't solve the halting problem.

How exactly do assumptions and constant expressions interact? [dcl.attr.assume] doesn't have any wording on this.


1) Note: extremely_complicated(x) is not a constant expression, but it's located in an assumption whose failure would result in UB within a constant evaluation of div(4, 2), which is a constant expression. It is generally said that UB in a constant expression needs to be diagnosed.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 1
    "its evaluation takes place in `div(4, 2)`" No it doesn't. Its evaluation doesn't take place at all. Your logic might be ok but your explanation of it is imprecise making it hard to follow. – Ben Voigt Sep 01 '23 at 18:50
  • @BenVoigt fair point; I've updated it again. Its hard to get the wording right because of all the hypotheticals involved. – Jan Schultke Sep 01 '23 at 18:57

2 Answers2

5

There is a specific exception for assume in the final C++23 draft.

[expr.const]/5.8 (with references deciphered)

An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

  • ...
  • an operation that would have undefined behavior as specified in [intro] through [cpp], excluding [dcl.attr.assume];

So a compiler doesn't need to judge the veracity of assumptions in order to judge the constantness of expressions. If you write

constexpr int g() {
    [[assume(false)]];
    return 5;
}

then g() may or may not be a core constant expression (it is unspecified whether [[assume(E)]]; disqualifies an expression for being constant if E both is allowed in the constexpr context and doesn't return true). If you further write

int main() {
    constexpr int x = g();
}

there are two cases. If the implementation has decided g() is not a core constant expression (as it is free to do so), it is required to give a diagnostic. If the implementation has decided g() is a core constant expression, the program has undefined behavior.

So you see that compilers have been given an out. A false assumption in a constexpr context can be undefined behavior and not a diagnostic at the implementation's choosing. An implementation could just choose to never check the assumptions in constant expressions. If an assumption then turns out to be false, the program appearing to compile and run successfully (if incorrectly) is just a manifestation of the resulting undefined behavior.

HTNW
  • 27,182
  • 1
  • 32
  • 60
0

First of all, even if the compiler can't solve the Collatz Conjecture and the Halting Problem, it is undefined behavior if those expressions don't evaluate to true.

If the converted expression would evaluate to true at the point where the assumption appears, the assumption has no effect. Otherwise, the behavior is undefined.

- [dcl.attr.assume]

The compiler might not be able to do anything with an assumption, but that doesn't matter to the C++ standard. However, since some assumptions are obviously too complex to diagnose, the standard has this paragraph:

It is unspecified whether [the expression] E is a core constant expression if E satisfies the constraints of a core constant expression, but evaluation of E would evaluate

  • [...]
  • a statement with an assumption whose converted conditional-expression, if evaluated where the assumption appears, would not disqualify E from being a core constant expression and would not evaluate to true.

[Note: E is not disqualified from being a core constant expression if the hypothetical evaluation of the converted conditional-expression would disqualify E from being a core constant expression. — end note]

- [expr.const] p5.33

What this means is:

  • [[assume( (std::exit(0), B) )]] does not disqualify E from being a core constant expression, regardless whether B is true or false
  • it is unspecified whether [[assume(extremely_complicated(x))]] disqualifies div from being a core constant expression
    • however, only if a hypothetical evaluation wouldn't exceed implementation-defined limits for constant expressions; if so, it cannot disqualify E

Compilers can implement this very simply, by either

  • ignoring assumptions in constant expressions, so that none disqualify E from being a core constant expression, or
  • selectively evaluating some assumptions1), and if they don't evaluate to false, don't disqualify E, otherwise raise an error

1) This cannot change observed behavior, such as by blowing an implementation-defined limit. An evaluation can take place, but it needs to behave as if it was a hypothetical evaluation.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • But now, assuming that `extremely_complicated(x)` is sufficiently complicated, then evaluating it _would_ disqualify the expression from being a constant expression (p5.7) and hence p5.33 doesn't apply. – user17732522 Sep 01 '23 at 16:56
  • @user17732522 no, it wouldn't. It is unspecified whether it is would disqualify E from being a constant expression. Remember that assumptions are not evaluated. If the compiler decides to evaluate the assumption in a constant expression (and to my knowledge, none do this), then the observable behavior would need to be as if it wasn't evaluated. – Jan Schultke Sep 01 '23 at 17:00
  • Yes, but it is only _unspecified_ whether or not the expression is a constant expression "_if [the assumption] evaluated where the assumption appears, would not disqualify E from being a core constant expression and [...]_". If the evaluation _would_ exceed implementation limits, then it is not _unspecified_ whether or not the evaluation is a constant expression. Instead the compiler _must_ treat the assumption as if it wasn't present (because p5.8 excludes UB due to assumptions). – user17732522 Sep 01 '23 at 17:04
  • @user17732522 that's not what the standard says. It says *"a statement with an assumption [...], if evaluated"*. The assumption is not evaluated, the null-statement that it applies to is. The evaluation of the assumption is purely hypothetical. – Jan Schultke Sep 01 '23 at 17:07
  • Its about what is also stated in the note that you are quoting. For example neither `[[assume( (std::exit(0), true) )]]` nor `[[assume( (std::exit(0), false) )]]` can disqualify your expression under consideration from being a constant expression because both `(std::exit(0), true)` and `(std::exit(0), true)` would disqualify from being a constant expression if, hypothetically, they were evaluated where they appear. So if evaluation of `extremely_complicated(x)` would, hypothetically, exceed implementation limits, then the compiler _can't_ decide that `div(4, 2)` isn't a constant expression. – user17732522 Sep 01 '23 at 17:19
  • "_This cannot change observed behavior,_": It can, because you can make SFINAE/overload resolution choices based on whether an expression is a constant expression. – user17732522 Sep 01 '23 at 17:20
  • @user17732522 I see what you mean now. I've updated the answer yet again. And I mean that the evaluation of an assumption can't change observable behavior because it's supposed to be hypothetical. Of course, in practice, the quasi-hypothetical evaluation that the implementation performs can actually change the behavior, but this is all in accordance with the as-if rule. The standard-mandated behavior would be exactly the same if the implementation could magically prove/disprove all assumptions. – Jan Schultke Sep 01 '23 at 17:31
  • "_The standard-mandated behavior would be exactly the same if the implementation could magically prove/disprove all assumptions._": Sure, but there isn't anything to prove anyway. If at all, evaluation of the assumption with a given set of arguments is relevant. There is nothing in the feature that requires a proof of termination, of a universal or existential quantification of an expression or anything like that in any case, either as requirement or as the intended purpose. – user17732522 Sep 01 '23 at 17:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/255145/discussion-between-jan-schultke-and-user17732522). – Jan Schultke Sep 01 '23 at 17:38