1

I was trying to figure out the restrictions of constexpr in cpp11/14. There are some usage requirements I found in CPP14-5.19-4:

A constant expression is either a glvalue core constant expression whose value refers to an object with static storage duration or to a function, or a prvalue core constant expression whose value is an object where, for that object and its subobjects:

  • ...
  • if the object or subobject is of pointer type, it contains the address of another object with static storage duration, the address past the end of such an object (5.7), the address of a function, or a null pointer value.

I've run some tests(code shown below) for expressions that involves address-of operator &, in order to ensure the correctness of the standards' statements quoted above.

Simply put, I tried to take the address of a global int variable global_var, which is an object with static storage duration(if I was not thinking wrong), everything works just as standards points out. But, what confused me is that, when I tried to assign another pointer-type object(global_var_addr1 in code), which stored the address of the same object global_var, the program won't compile. And GCC says:

error: the value of ‘global_var_addr1’ is not usable in a constant expression
note: ‘global_var_addr1’ was not declared ‘constexpr’

, while Clang-Tidy says:

error: constexpr variable 'x2' must be initialized by a constant expression [clang-diagnostic-error]
note: read of non-constexpr variable 'global_var_addr1' is not allowed in a constant expression

and I don't know why, is there anything I missed?

So my question is:

1. Why, in a constant expression, I cannot use a pointer-type object which contains the address of an object with static storage duration, as standards says?
2. Why everything goes different in the same context as (1), when the object is auto specified?

Any advices would be welcomed, thanks in advance!


Code:

const int global_var_c = 123;
int global_var = 123;
const void *global_var_addr1 = &global_var;
const void *global_var_addr2 = nullptr;
auto global_var_addr3 = nullptr;

auto  main() -> int
{
    constexpr const int x00 = global_var_c;           // OK
    constexpr const void *x0 = &global_var;           // OK

    // Operate on the object of pointer type
    constexpr const void *x1 = &global_var_addr1;      // OK
    constexpr const void *x2 = global_var_addr1;       // ERROR: read of non-constexpr variable 'global_var_addr1'...

    // Operate on nullptr
    constexpr const void *x3 = &global_var_addr2;     // OK
    constexpr const void *x4 = global_var_addr2;      // ERROR: read of non-constexpr variable 'global_var_addr2'...

    // Operate on nullptr (with type deduction)
    constexpr const void *x5 = global_var_addr3;      // OK
    constexpr const void *x6 = &global_var_addr3;     // OK
}
absuu
  • 340
  • 1
  • 11

2 Answers2

5

In both

constexpr const void *x2 = global_var_addr1;

and

constexpr const void *x4 = global_var_addr2;

a lvalue-to-rvalue conversion happens from the variable global_var_addr1/global_var_addr2 glvalue to the pointer value they hold. Such a conversion is only allowed if the variable's lifetime began during the evaluation of the constant expression (not the case here) or if it is usable in constant expressions, meaning that it is constexpr (not the case here) or initialized by a constant expression (is the case here) and of reference or const-qualified integral/enumeration type (not the case here).

Therefore the initializers are not constant expressions.


This is different in the case of

constexpr const int x00 = global_var_c;

since global_var_c is of const-qualified integral type.


I am not exactly sure about

constexpr const void *x5 = global_var_addr3;      // OK

Intuitively it should work, because the type of nullptr and consequently the deduced type of global_var_addr3 is std::nullptr_t which doesn't need to carry any state, so that a lvalue-to-rvalue conversion wouldn't be necessary. Whether the standard actually guarantees that, I am not sure at the moment.

Reading the current wording (post-C++20 draft), [conv.ptr] specifies only conversion of a null pointer constant (i.e. a prvalue of std::nullptr_t) to another pointer type and [conv.lval] specifically states how the lvalue-to-rvalue conversion of std::nullptr_t produces a null pointer constant. [conv.lval] also clarifies in a note that this conversion doesn't access memory, but I don't think that makes it not a lvalue-to-rvalue conversion given that it still written under that heading.

So it seems to me that strictly reading the standard

constexpr const void *x5 = global_var_addr3;      // OK

should be ill-formed (whether global_var_addr3 is const-qualified or not).

Here is an open clang bug for this. There seems to be a link to come internal discussion by the standards committee, which I cannot access.


In any case, the auto placeholder doesn't matter. You could have written std::nullptr_t for it instead directly.

All of these are requirements for being a core constant expression, which is a prerequisite to the requirements you mention in your question.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • _No lvalue-to-rvalue conversion is required to copy that type_ Which paragraph says this? – Language Lawyer Dec 27 '21 at 11:05
  • @LanguageLawyer Right, I am not actually sure it says that anywhere. I rewrote the part of the answer until I find something definitive. – user17732522 Dec 27 '21 at 11:10
  • Clang verstion 8.0.1 and earlier doesn't accept the `nullptr_t`-case https://godbolt.org/z/dPGE6qM1q – Language Lawyer Dec 27 '21 at 11:16
  • We know [conv.lval] special cases `std::nullptr_t` (an lvalue-to-rvalue conversion on an object with type *cv* `std::nullptr_t` results in a null pointer constant), but [expr.const] defining a core constant expression doesn't mention `std::nullptr_t`, and if `constexpr const void *x5 = global_var_add3` does do an lvalue-to-rvalue conversion (which I suspect it does), it is not a constant expression. – Artyer Dec 27 '21 at 11:25
  • @Artyer Yes, this would also be my understanding. I have updated the answer – user17732522 Dec 27 '21 at 11:46
  • @LanguageLawyer There is an open regression bug filed in LLVM's bug tracker for that change. I have updated my answer with how I see it after reading the standard again. – user17732522 Dec 27 '21 at 11:48
  • @user17732522 "*Such a conversion is only allowed if ... or initialized by a constant expression (is the case here)*" Can I have some standards reference related to this constraint? – absuu Dec 27 '21 at 13:01
  • @absuu [\[expr.const\]](https://timsong-cpp.github.io/cppwp/n4868/expr.const) in the post-C++20 draft. (5.8.1) is the _usable in constant expressions_ requirement on lvalue-to-rvalue conversion in core constant expressions, (4) defines _usable in a constant expression_ for variables as _constant initialized_ (initialized by a constant expression) and _potentially constant_ plus some modules related requirements. (2) gives the exact definition of _constant initialized_ and (3) defines _potentially constant_ with the type requirement in the absence of `constexpr`. – user17732522 Dec 27 '21 at 13:12
  • @user17732522 Thanks for the precise references! I'm kind of new to cpp and carefully read through what you provided. So, is it rigorous to say that : in my example, `global_var_addr1` is *potentially-constant* because its intializer `&global_var` is a constant expression. But since I haven't specified `global_var_addr1` as an expression that is *usable-in-constant-expression*, by explicit specifier `constexpr`, it remains *potentially-constant* property, along with which the *lvalue-to-rvalue conv* happened on it cannot result in an constant expression, according to *[expr.const]-5.81* ? – absuu Dec 27 '21 at 13:58
  • @absuu `global_var_addr1` is _constant-initialized_ for the reason you gave, but not _potentially-constant_ because it is not `constexpr`, not a reference and not a `const`-qualified integral or enumeration type. Adding `constexpr` to `global_var_addr1` would satisfy this additional condition and both together make it _usable-in-constant-expressions_. Without the full _usable-in-constant-expression_ requirement applying lvalue-to-rvalue conversion to it is not allowed in a constant expression. – user17732522 Dec 27 '21 at 14:17
  • @absuu Practically speaking, it is easier to remember that you cannot use the value of any variable not marked `constexpr` in a constant expression. As we discussed there are some exceptions, but these are corner cases which you probably don't want to rely on. – user17732522 Dec 27 '21 at 14:19
  • @user17732522 Thanks for the pratically advice again! One thing that I'm still not much sure is that, accoding to the reference *[expr.const]-11* and *[expr.const]-11.2*, can I view `x0` as *a glvalue core constant expression*, and `&global_var` as *a prvalue core constant expression*? – absuu Dec 27 '21 at 14:36
  • @absuu Yes, I think that is correct. – user17732522 Dec 27 '21 at 14:56
  • @user17732522 Deeply appreciate for your kindly patience, and actually don't know how to express my appreciation more than an acceptance, just...THANK YOU again! – absuu Dec 27 '21 at 15:09
1

The variable declared here is clearly not constexpr (nor even const):

const void *global_var_addr1 = &global_var;

And you can't use non-constexpr values to initialize constexpr values. So it's no surprise this fails to compile:

constexpr const void *x2 = global_var_addr1; // ERROR: read of non-constexpr

The address of a non-constexpr value can be used in cases like you've shown, however, but the value stored in a variable and the address of a variable are not the same thing.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436