Document #: | P3588R1 |
Date: | 2025-05-17 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Brian Bi <bbi10@bloomberg.net> |
Local and unnamed classes (as well as classes nested within) are not permitted to declare static data members. This restriction dates back to C++98, when there was no way to provide a definition for such a member. In modern C++, static data members can have inline definitions, so this rationale is obsolete. Static data members can be useful in local classes for the same reasons why they are useful in non-local classes, so this paper proposes to allow them in C++29.
C++03 §[class.static.data] stated as follows:
[…] Unnamed classes and classes contained directly or indirectly within unnamed classes shall not contain
static
data members. [Note: this is because there is no mechanism to provide the definitions for suchstatic
data members. ][…] A local class shall not have
static
data members.
No rationale is explicitly provided for the latter restriction. However, we can see that “there is no mechanism to provide the definitions” applies to static data members of local classes as well. There was one additional possible issue, which is the need to generate an external symbol based on the identity of the local class, but it seems that this applies equally well to functions (i.e., it seems that a member function of a local class in an inline function would need to have a weak external symbol since its address could be taken), and in any case, thanks to [N2657], unnamed and local classes became valid template arguments from C++11 and it is certain that all implementations can generate appropriate mangled names for local classes and members of local and unnamed classes. There do not appear to be any remaining technical obstacles to allowing such classes to have static data members.
[CWG728] proposed to relax some restrictions on local classes; more specifically, to allow them to be templates, declare member templates, declare friends, and declare static data members. It was determined that this was a feature request, and a paper was submitted, [P2044R2]. However, that paper addressed only the issue of member templates. This paper deals only with the simpler feature of static data members.
C++ entities should generally be declared in the narrowest scope in
which they are needed, which may be a single function. For example, in
Google Test, the TEST
macro is used
to define a single function which then verifies some expected
properties. If a class is required only for that single test, it is
convenient to declare it as a local class. It can be desirable for such
a local class to have a static data member, such as a static data member
that tracks the number of live objects of the class.
Here is another example: suppose that a C++ library defines a custom
version of the tuple protocol in which a tuple-like class is expected to
declare a static data member named
tuple_size
, initialized to a
compile-time constant. If a unit test needs to declare such a class and
pass it as a template argument to the component under test (which is
expecting it to be a tuple), it would be convenient to be able to scope
that class to the smallest enclosing block.
In the former case, the static data member would be declared static inline
.
In the latter case, it would be declared static constexpr
,
which makes it implicitly inline. A non-inline static data member in a
local class cannot be defined, but I see no reason not to also allow it.
It could be used just for its type (i.e., in unevaluated
contexts only). Allowing such a variable to inhabit class scope would
make it more narrowly scoped than if it were a local
extern
variable inhabiting the enclosing function.
Note that although the current language does not permit static data members in local classes, it does permit static local variables in member functions of local classes. That is, the Meyer singleton pattern can be contained within an outer block scope. This workaround for the inability to declare static data members in local classes adds verbosity in requiring a function to be defined and called. Meyer singletons also trade off some performance for safety: additional instructions are executed every time the function containing the static local variable is called (unless the variable is constant-initialized), but most cases of initialization order bugs are prevented. Although the additional safety is useful, having to incur changes in performance as a cost of moving static variables within a block scope violates the C++ design principle that you don’t pay for what you don’t use.
Allow static data members to be declared by local and unnamed classes without restriction, except unnamed classes that have a typedef name for linkage purposes. That is, the following would remain ill formed:
typedef struct {
inline static int x = 0;
} S;
§9.2.4 [dcl.typedef]1p5 places various restrictions on such classes in the vein of requiring them to be “C-like”: for example, they can’t have member functions nor member typedefs. Since static data members don’t exist in C, I don’t propose to drop this restriction.
Likewise, restrictions on anonymous unions (§11.5.2 [class.union.anon]p1) are left untouched by this proposal. Being a compatibility feature with C, they aren’t allowed to declare any members other than public non-static data members.
Note that static data members may be thread-local, and therefore thread-local data members will also become allowed in local classes under this proposal. However, the current Standard doesn’t specify initialization order for thread-local namespace-scope variables ([CWG2914], [CWG2928]). The intent of this proposal is to be orthogonal to those Core issues: just as static data members of local classes are to be initialized in the same time as if they were at namespace scope, the same is meant to hold for thread-local members of local classes.
I propose that static data members of local classes follow the same
initialization order rules as if their classes were non-local: for
example, if x
,
y
, and
z
are three inline variables and in
every translation unit they appear in that order, and
x
and
z
belong to non-local classes while
y
belongs to a local class, then
y
is initialized after
x
and before
z
, unless
y
’s enclosing function is templated,
in which case y
could be initialized
in any order relative to x
and
z
. I believe that this rule is
intuitive and any other rule would be a source of bugs: if a
later-initialized static variable could be visible at the point of
declaration of an earlier-initialized one, the earlier-initialized
variable could use the later-initialized variable’s value before the
latter has been initialized.
It is worth noting that lexical initialization order conflicts with a principle of C++ classes, that their behavior should not change depending on the location of the class’s complete-class contexts. Consider the following example:
struct C {
int f();
static inline int a = f(); // `b` is declared later and not yet initialized
};
int C::f() {
struct S {
static inline int b = 1;
};
return S::b;
}
Here, a
would be initialized
prior to b
;
f
would return either 0 or 1 (see
§6.9.3.2
[basic.start.static]p3).
Evidently, lexical initialization order can’t prevent all
initialization order bugs, but the alternative, namely to guarantee that
static data members of class C
are
initialized before static data members appearing within complete-class
contexts of C
, would result in a
similar bug when f
is defined in
line:
struct C {
void f() {
struct S {
static inline int b = 1;
};
}
static inline int a = f(); // `b` is declared in `f` and not yet initialized
};
Given that neither initialization order rule is perfect, I still advocate lexical order as the easiest to understand. A third alternative that avoids initialization order issues completely, but reduces the usefulness of the feature, is to require static data members of local classes to be constant-initialized and have constant destruction.2
To summarize, the three options are
If a function with internal or no linkage is never called and never has its address taken, implementations can currently skip code generation for the function. However, since I propose that the initialization of a static data member of a local class should occur as described in §6.9.3 [basic.start] (i.e. upon program or thread startup, unless deferred) it would be surprising if such an initialization having side effects could be skipped if the enclosing function isn’t called. Therefore, if there are any static data members defined inside the function, code generation must be done for those static data members and potentially for lambdas reachable from them.3
Note that for a local class, the definition of each member is always be instantiated eagerly when the enclosing function is instantiated; see Note 3 to §13.9.2 [temp.inst]p2. I don’t propose any carve-out to this rule for static data members.
GCC supports static data members in unnamed and local classes as an
extension when
-fpermissive
is used. GCC does not allow such members to be explicitly declared
inline
, but
this seems to be an oversight: the error message says “‘inline’
specifier invalid for variable ‘x’ declared at block scope”. GCC does
allow the member to be
constexpr
,
and generates an appropriate definition in that case (i.e., the
address can be taken).
I implemented this feature partially, including dynamic initialization of inline static data members, as a patch to Clang 20 (May 4, 2025). This implementation passes the Clang unit tests but has the following known bugs:
The initialization order is not correct; complete-class contexts are parsed later than the rest of the enclosing class, so the order in which static data member initializers are emitted corresponds to option 2 above, rather than option 1.
Static data members of local classes are currently not emitted in cases such as
// x appears at namespace scope
int x = [] { struct S { static inline int y = 0; }; return S::y; }();
because the code that iterates over definitions to emit them misses closure types that were injected into the enclosing namespace scope. (As a result, a program containing the above translation unit fails to link.)
In order to ensure lexical initialization order, it seems that an implementation might need to store the location of each non-templated static data member found while parsing a top-level class definition (i.e., one that does not have any enclosing class scope) and then sort them at the end of the class definition. This would be novel but not extremely difficult to implement. Such lists (of which multiple might be produced for every namespace-scope declaration) could also be used to fix the second bug.
Wording is relative to [N5008].
Modify §6.9.3.3 [basic.start.dynamic]p2 as follows to account for the fact that a definition of a static data member of a local class can be nested within the definition of some other variable requiring dynamic initialization.
For two definitions
D
andE
of non-block variables with static storage duration,A declarationD
is appearance-ordered beforea declarationE
if
D
appears in the same translation unit asE
, or- the translation unit containing
E
has an interface dependency on the translation unit containingD
,and
in either case prior tothe init-declarator ofD
ends before the init-declarator ofE
.
Strike §11.4.9.3 [class.static.data]p2. (The first sentence is redundant with §9.2.2 [dcl.stc]p8.)
A static data member shall not bemutable
([dcl.stc]). A static data member shall not be a direct member ([class.mem]) of an unnamed ([class.pre]) or local ([class.local]) class or of a (possibly indirectly) nested class ([class.nest]) thereof.
Strike §11.6 [class.local]p4.
[Note 2: A local class cannot have static data members ([class.static.data]). — end note]
Bengt Gustafsson, Arthur O’Dwyer, and Corentin Jabot provided valuable feedback on the motivation, wording, and implementation for this paper.
All citations to the Standard are to working draft N5008 unless otherwise specified.↩︎
All
constexpr
static variable declarations would necessarily satisfy this criterion,
but there are others that would as well, such as the example of an
instance counter, defined as static inline int count = 0;
;
the destruction of count
itself is
trivial.↩︎
If the behavior of my (partial) implementation exhibits any deviation from these rules, the deviation is unintentional.↩︎