Search code examples
c++constant-expression

What exactly is a C++ constant subexpression?


The C++ standard, Section 3.14 [1], says:

3.14[defns.const.subexpr] constant subexpression expression whose evaluation as subexpression of a conditional-expression CE would not prevent CE from being a core constant expression

I'm not sure how to interpret the above statement. For example, consider the below example:

#include <iostream>

struct X {
    int a_;
    X(int a) : a_{a} {}
};

auto f() {
    return X{0};
}
auto g() {
    return X{1};
}
int main() {
    auto i = 0;
    std::cin >> i;
    auto x = (i == 0) ? f() : g();      
}

What's the constant subexpression in auto x = (i == 0) ? f() : g()? What does it mean that it won't prevent the CE from being a core constant expression? Is (i == 0) ? f() : g() a constant expression?


Solution

  • First of all we need to understand the meaning of "core constant expression".

    Wait, no, actually, first of all we need to be clear on the fact that "expression" really has two different meanings. An expression is a grammar production whose definition can be found here. In the below function,

    int foo(int x) {
        return 2*x;
    }
    

    2, x, and 2*x can be found as expressions located in the function body. (Technically, only 2*x is actually produced by the expression grammar production in this case. 2 and x are primary-expressions.) However, the term "expression" also means "an instance of an evaluation of an expression". Under this meaning, every time foo is called, there are 2, x, and 2*x expressions that are different from the 2, x, and 2*x expressions from any other invocation.

    How do you know which meaning the word "expression" is being used with? You have to infer it from context.

    When we speak of a "core constant expression" the second meaning of "expression" is used. It is only possible to classify something as a core constant expression or not a core constant expression if that something is a specific instance of an evaluation of an expression. For the remainder of the answer the term "expression" will only be used in this sense.

    A core constant expression is an expression that, taken as a whole:

    • does not read from or write to any objects other than ones whose lifetime began within that expression; except that reading from variables whose lifetime begin before the expression is allowed if their values are compile-time constants (for example, constexpr variables).
    • does not perform any other evaluations that are forbidden in constant expressions under the rules of C++. For example, calling a function that is not constexpr is one such forbidden evaluation (this is what stops you from doing I/O at compile time; functions like getchar are obviously not constexpr).

    This first point is a bit subtle. It means we can have situations like this:

    constexpr int foo(int x) {
        return 2*x;
    }
    constexpr int bar = foo(21);  // 42
    

    Here, foo(21) is a core constant expression. As part of that expression, the value of x is read. But this is ok because x is created during the evaluation of foo(21).

    Equally, 2*x is not a core constant expression because it reads the value of x, and the lifetime of x began before the expression 2*x.

    Therefore, a core constant expression can involve the evaluation of expressions that are not themselves core constant expressions. Another way of putting it is that certain evaluations cannot be a "complete" constant evaluation but can be part of a larger enclosing constant evaluation.

    By the way, a "constant expression" is a core constant expression that satisfies a few additional criteria, which we won't discuss here because they're only tangentially relevant to the question.

    Now let's look at why we need the concept of a "constant subexpression". It came from LWG 2234, which asked LWG to clarify the fact that assert(b) can sometimes be used in constant expressions. In particular, we want to focus on the case where NDEBUG is not defined and where b is true. Basically, what we want to say is that if the evaluation of b doesn't violate any of the rules of constant evaluation, then neither does assert(b), since it'll just evaluate b, see that it's true, and then do nothing. But how do we say that?

    At first it might seem that we basically want to say something like this (not quite standardese):

    if reading the value of b would be a core constant expression and its result is true, then assert(b) is also a core constant expression.

    This captures the idea that assert(b) doesn't perform any additional operations that could disqualify the evaluation from being part of an enclosing constant evaluation. However, you'll see in the issue description that this doesn't really go far enough. We can have cases like this:

    constexpr void check(bool b) { assert(b); }
    // ...
    constexpr int x = (check(true), 0);
    

    The initializer for the variable x (by dint of the fact that x is constexpr) must be a constant expression and that implies that it must also be a core constant expression. This core constant expression creates the variable b, but the result of performing an lvalue-to-rvalue conversion on b (i.e., reading its value) isn't a core constant expression. Nevertheless, as discussed above, it is permitted to be done in the context of the enclosing core constant expression.

    Here we see that the previously proposed criterion doesn't apply, because reading the value of b is not a core constant expression; nevertheless, it's something that is permitted to be evaluated within an enclosing core constant expression, and the wording should ensure that the same is true for assert(b).

    So that's how the concept of "constant subexpression" was arrived at. It allows us to express ideas such as "if the evaluation of E would not prevent a particular enclosing evaluation from being a core constant expression, then the evaluation of f(E) also will not prevent the same enclosing evaluation from being a core constant expression" by saying "if E is a constant subexpression, then so is f(E)".

    This implies that an expression can only be a constant subexpression relative to something, i.e., we have to say what the enclosing expression is, before we can say whether we have a constant subexpression for that enclosing expression (meaning that that subexpression doesn't prevent that enclosing expression from being a core constant expression). In the above example, assert(b) is a constant subexpression relative to the enclosing core constant expression (check(true), 0).

    In the OP's question, taking the whole initializer of x as the potential core constant expression, we see that the subexpressions i == 0, f(), and g() are all not constant subexpressions:

    • i == 0 prevents the enclosing evaluation from being a core constant expression because it reads the value of the non-const variable i.
    • f() and g() call the non-constexpr constructor X::X(int).

    However, 0 is a constant subexpression of the initializer of x.