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.