Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why sometimes local class cannot access constexpr variables defined in function scope

This c++ code cannot compile:

#include <iostream>

int main()
{
    constexpr int kInt = 123;
    struct LocalClass {
        void func(){
            const int b = std::max(kInt, 12); 
            //                     ^~~~  
            // error: use of local variable with automatic storage from containing function
            std::cout << b;
        }
    };
    LocalClass a;
    a.func();
    return 0;
}

But this works:

#include <iostream>
#include <vector>

int main()
{
    constexpr int kInt = 123;
    struct LocalClass {
        void func(){
            const int b = std::max((int)kInt, 12); // added an extra conversion "(int)"
            std::cout << b;
            const int c = kInt; // this is also ok
            std::cout << c;
            const auto d = std::vector{kInt}; // also works
            std::cout << d[0];
        }
    };
    LocalClass a;
    a.func();
    return 0;
}

Tested under C++17 and C++20, same behaviour.

like image 386
flm8620 Avatar asked Apr 08 '26 01:04

flm8620


2 Answers

1. odr-using local entities from nested function scopes

Note that kInt still has automatic storage duration - so it is a local entity as per:

6.1 Preamble [basic.pre]
(7) A local entity is a variable with automatic storage duration, [...]


In general local entities cannot be odr-used from nested function definitions (as in your LocalClass example)

This is given by:

6.3 One-definition rule [basic.def.odr]
(10) A local entity is odr-usable in a scope if:
[...]
(10.2) for each intervening scope between the point at which the entity is introduced and the scope (where *this is considered to be introduced within the innermost enclosing class or non-lambda function definition scope), either:

  • the intervening scope is a block scope, or
  • the intervening scope is the function parameter scope of a lambda-expression that has a simple-capture naming the entity or has a capture-default, and the block scope of the lambda-expression is also an intervening scope.

If a local entity is odr-used in a scope in which it is not odr-usable, the program is ill-formed.

So the only times you can odr-use a local variable within a nested scope are nested block scopes and lambdas which capture the local variable.

i.e.:

void foobar() {
    int x = 0;

    {
        // OK: x is odr-usable here because there is only an intervening block scope
        std::cout << x << std::endl;
    }

    // OK: x is odr-usable here because it is captured by the lambda
    auto l = [&]() { std::cout << x << std::endl; };

    // NOT OK: There is an intervening function definition scope
    struct K {
      int bar() { return x; }
    };
}

11.6 Local class declarations [class.local] contains a few examples of what is and is not allowed, if you're interested.


So if use of kInt constitutes an odr-use, your program is automatically ill-formed.

2. Is naming kInt always an odr-use?

In general naming a variable constitutes an odr-use of that variable:

6.3 One-definition rule [basic.def.odr]
(5) A variable is named by an expression if the expression is an id-expression that denotes it. A variable x that is named by a potentially-evaluated expression E is odr-used by E unless [...]

But because kInt is a constant expression the special exception (5.2) could apply:

6.3 One-definition rule [basic.def.odr]
(5.2) x is a variable of non-reference type that is usable in constant expressions and has no mutable subobjects, and E is an element of the set of potential results of an expression of non-volatile-qualified non-class type to which the lvalue-to-rvalue conversion is applied, or

So naming kInt is not deemed an odr-use as long as it ...

  • is of non-reference type (✓)
  • is usable in constant expressions (✓)
  • does not contain mutable members (✓)

and the expression that contains kInt ...

  • must produce a non-volatile-qualified non-class type (✓)
  • must apply the lvalue-to-rvalue conversion (?)

So we pass almost all the checks for the naming of kInt to not be an odr-use, and therefore be well-formed.

The only condition that is not always true in your example is the lvalue-to-rvalue conversion that must happen.

If the lvalue-to-rvalue conversion does not happen (i.e. no temporary is introduced), then your program is ill-formed - if it does happen then it is well-formed.

// lvalue-to-rvalue conversion will be applied to kInt:
// (well-formed)
const int c = kInt;  
std::vector v{kInt}; // vector constructor takes a std::size_t

// lvalue-to-rvalue conversion will NOT be applied to kInt:
// (it is passed by reference to std::max)
// (ill-formed)
std::max(kInt, 12); // std::max takes arguments by const reference (!)

This is also the reason why std::max((int)kInt, 12); is well-formed - the explicit cast introduces a temporary variable due to the lvalue-to-rvalue conversion being applied.

like image 147
Turtlefight Avatar answered Apr 10 '26 13:04

Turtlefight


Language-lawyer answer: std::max(kInt, 12) odr-uses kInt, since std::max accepts a constant reference which must be initialized by [dcl.init.ref]/1, by [dcl.init.ref]/5.1. However, std::max((int)kInt, 12) does not odr-use kInt by [basic.def.odr]/5.2. main()::LocalClass cannot odr-use kInt by [class.local]/1.


std::max takes its parameters by const reference (here int const&), and returns that reference passed through.

So, in const int b = std::max(kInt, 12);, kInt is a reference to the automatic object main()::kInt; main()::LocalClass::func() has no way to access the stack frame of main() so it is unable to form that reference. This is fortunate, since otherwise kInt could be a dangling reference (if you were to call LocalClass::func() after main() returns). For example, this function has a dangling reference bug:

auto f() {
    constexpr int kInt = 123;
    return [&](int i) { return std::max(i, kInt); };
    //                                     ^^^^ dangling reference to f()::kInt
}

Casting kInt to int performs lvalue-to-rvalue conversion, which in this case bypasses accessing the storage of kInt since the compiler knows that it is constexpr and cannot take any other value than 123.

like image 31
ecatmur Avatar answered Apr 10 '26 15:04

ecatmur



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!