11

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.

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
flm8620
  • 1,411
  • 11
  • 15
  • Specific compilers involved (gcc, clang, msvc)? Tested against others? – πάντα ῥεῖ Jun 01 '23 at 16:16
  • 3
    Is it legal for `func` to try to access _any_ variables of automatic storage duration declared in `main`, `constexpr` or otherwise? – Paul Sanders Jun 01 '23 at 16:37
  • 2
    I think the both codes are illegal. `kInt` must be static. The related topic https://stackoverflow.com/questions/13865842/does-static-constexpr-variable-inside-a-function-make-sense – 273K Jun 01 '23 at 16:50
  • 4
    Because local class can't odr-use local entities. Because if L2R-conversion is/«yields» a constant expression, it is not odr-use. I think there should be plenty of dups about these things. – Language Lawyer Jun 01 '23 at 16:56

2 Answers2

5

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.

Turtlefight
  • 9,420
  • 2
  • 23
  • 40
2

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.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 4
    The question is language-lawyered. There is no such thing as a "stack frame" in the standard C++ document. Trying to explain a language-lawyered question using implementation details is wrong. – Jason Jun 01 '23 at 16:34
  • @Jason added language lawyering. – ecatmur Jun 01 '23 at 17:20
  • 1
    @Jason Stack frame is a valid universal programming concept and not a detail. The details would be the region allocated to space, the direction of growth, etc. Also the C++ standard is not a specification, so it's better to explain stuff in meaningful terms. – curiousguy Jun 02 '23 at 22:14
  • 1
    @curiousguy The point is that when a question is tagged `language-lawyer`, the answerer is expected to include references the corresponding standard. Any additional terms(universal or not) are welcomed but they are entirely optional. The main thing is to quote the corresponding standard reference. First and foremost a reference to the standard should be included and then you can optionally include non-normative terms or terms whose implementation might differ between platforms etc. – Jason Jun 03 '23 at 04:54
  • 1
    @Jason Yes and no. It should be explained why it has to be the way it is, according to black on white text, according to intent, or to common sense. But remember that common sense always beat letter. – curiousguy Jun 03 '23 at 13:02
  • 1
    @Jason I've been thinking about this a bit, and I think it's fairly misleading to just stick a "language-lawyer" tag on a question and expect a completely different kind of answer. Tags should be an aid to navigating questions, not a replacement for them. – ecatmur Jun 04 '23 at 04:41
  • @ecatmur No, there is nothing misleading about asking/looking for an authoritative/genuine answer. The language-lawyer tag is there for a reason. – Jason Jun 09 '23 at 15:12
  • 1
    @Jason you're completely missing the point. Adding a tag to a question and expecting a completely different set of answers to result is what's misleading. Would it be acceptable for an exact duplicate of this question to exist but without that tag, since the answers should then be different? – ecatmur Jun 10 '23 at 10:13