1. Introduction
The
predefined macro is a common language extension for C and C++ which expands to an integer literal
that starts at
and increments by
every time it is expanded in a translation unit. This is useful for generating
unique identifiers, generating unique indices, and other preprocessor metaprogramming uses.
2. Previous Proposals
has not been proposed to WG21, however, it was briefly mentioned previously in a WG14 paper, Extensions
to the preprocessor for C2Y [N3190]. The meeting minutes, [N3227], mention brief discussion of
’s
implications on evaluation order and caching. No polls were taken on [N3190].
This paper provides a focused proposal on
and aims to provide additional context and motivation.
3. Rationale for Standardization
is de-facto portable today. Every major implementation supports it with unsurprising semantics. However,
there is inherent uncertainty surrounding its portability and semantics due to it not being standardized.
Codebases striving for maximum portability must resort to detection and fallback such as this example from google benchmark:
// Check that __COUNTER__ is defined and that __COUNTER__ increases by 1 // every time it is expanded. X + 1 == X + 0 is used in case X is defined to be // empty. If X is empty the expression becomes (+1 == +0). #if defined(__COUNTER__) && (__COUNTER__ + 1 == __COUNTER__ + 0) #define BENCHMARK_PRIVATE_UNIQUE_ID __COUNTER__ #else #define BENCHMARK_PRIVATE_UNIQUE_ID __LINE__ #endif
Meanwhile other C++ codebases avoid the macro altogether due to this uncertainty. In the absence of cautious checking
and fallback, a developer must consult numerous widely used C++ implementations to convince themselves that
exists and does what they want.
In the case of google benchmark,
is an adequate fallback due to how
macros are typically used.
However, this is not an adaquate general-purpose replacement due to it not being unique in the general case.
While every major C++ compiler today supports
, it’s not always enabled. For example, EDG only provides it
outside of standards mode.
Additionally, minor divergences in
semantics are observable (see § 6.5 Argument Handling), though they do
not impact most use cases.
Due to fairly widespread use, both in C and C++, it would be useful to incorporate the existing practice of
into the official standard in order to provide more clear portability and semantic guarantees.
4. Motivating Examples
A brief survey of some uses of
in the C and C++ community:
C++:
-
Google benchmark uses
for unique identifiers, falling back to__COUNTER__
if__LINE__
isn’t present or doesn’t behave as expected__COUNTER__ -
Google Orbit uses
for unique identifiers__COUNTER__ -
LLVM uses
for unique identifiers as well as in sanitizer code to prevent ICF__COUNTER__ -
Catch2 uses
for unique identifiers, falling back to__COUNTER__ __LINE__ -
Tensorflow uses
extensively, primarily for unique identifiers__COUNTER__ -
Chromium uses
for unique identifier generation, e.g. in crash logging code, as well as for creating unique tags for__COUNTER__
sABORT () -
Folly uses
for unique identifiers, falling back to__COUNTER__
if not present__LINE__ -
v8 uses
for unique identifiers__COUNTER__
C:
-
Linux uses
for, among other things:__COUNTER__ -
Systemd uses
for unique identifiers in multiple places link 1 link 2__COUNTER__ -
Netdata uses
for unique identifiers__COUNTER__ -
The QMK firmware project uses
auto-incrementing endpoint numbers; link__COUNTER__ -
Yabai, a macOS window manager, uses
for unique indices in timing utilities; link__COUNTER__ -
Drgn, a programmable debugger, uses
for unique identifiers througout the codebase; link__COUNTER__ -
Metric Panda Games uses
for lookup tables as part of a localization and compile-time string hashing system.__COUNTER__
Many additional uses include use for static assertions, however, that use case is now covered by built-in static assertion facilities.
5. Implementation Support
has long been supported by all major implementations of C and C++:
Compiler | Earliest Version On Compiler Explorer |
---|---|
GCC | 3.4.6 ❌ (earliest version supporting : 4.4.7 ✔️)
|
Clang | 3.0.0 ✔️ |
MSVC | 19.0 ✔️ |
ICC | 13.0.1 ✔️ |
ICX | 2021.1.2 ✔️ |
EDG | 6.5 🟡 |
🟡: Supported only outside standards mode, requiring either microsoft, GCC, or Clang emulation mode to be enabled
(controlled with
,
, and
respectively).
Comparison: https://godbolt.org/z/fqTs9sWx6
Additionally, C compiler support excluding duplicates from above:
Compiler | Earliest Version On Compiler Explorer |
---|---|
cc65 | 2.17 ❌ |
Chibicc | 2020-12-07 ✔️ |
CompCert | 3.9 ✔️ |
Cproc | Trunk ✔️ |
EDG | 6.5 🟡 |
Movfuscator | Trunk ✔️ |
ppci | 0.5.5 ❌ |
SDCC | 4.0.0 ✔️ |
TCC | 0.9.27 ✔️ |
TenDRA | Trunk ❌ |
z88dk | 2.2 ✔️ |
Zig cc | 0.6.0 ✔️ |
Comparison: https://godbolt.org/z/Mx4MznMaY
6. Design Considerations
6.1. Precompiled Headers
MSVC and GCC save the state of
in precompiled headers. GCC notes that the
macro must not be
expanded prior to inclusion of a pre-compiled header. If it is, then the precompiled header is not used.
This paper proposes no requirements for
behavior surrounding pre-compiled headers.
6.2. Modules
GCC and MSVC do not propagate
across modules, including for header units. The following compiles with a
linker error due to multiple definitions of
:
// header.hpp #define CONCAT_IMPL(x, y) x##y #define CONCAT(x, y) CONCAT_IMPL(x, y) #define NEW_VAR(name) CONCAT(name, __COUNTER__) int NEW_VAR ( x ); // x0 int NEW_VAR ( x ); // x1 // main.cpp import "header.hpp" int NEW_VAR ( x ); // x0
There are similar concerns with
and
macros surrounding header units, though the potential for
problems is less pronounced. One option would to disallow the expansion of
in header units, however, no
such restriction is proposed in this paper.
This paper proposes no change to the current behavior. Other behaviors would introduce additional complexity without clear benefit.
6.3. ODR
It’s possible to inadvertently violate ODR with
:
// foo.hpp #define CONCAT_IMPL(x, y) x##y #define CONCAT(x, y) CONCAT_IMPL(x, y) #define NEW_VAR(name) CONCAT(name, __COUNTER__) inline void foo () { int NEW_VAR ( x ) = 2 ; } // a.cpp #include "foo.hpp"// b.cpp int x = __COUNTER__ ; #include "foo.hpp"
Current implementations do not make any special attempt to diagnose or prevent such use of
beyond existing
ODR diagnostics. Similar ODR issues can occur as a result of
and
. While existing practice is that
these ODR issues exist, it is worthwhile looking at possible solutions to the problem.
N.b.: Similar considerations exist for C, however, different
function definitions can only result in
unspecified behavior.
6.3.1. Possible Solutions
This is a difficult problem to solve due to the nature of
and how the preprocessor interacts with the rest
of the language. Possible solutions include:
-
Just don’t use
in__COUNTER__
functions in headersinline -
Provide a mechanism to reset the
, or even push and pop the counter__COUNTER__ -
Allow for multiple counters
, possibly tied to__COUNTER__ __FILE__ -
Change ODR to allow
andinline void foo () { int x0 ; }
to not be ill-formedinline void foo () { int x1 ; } -
Some sort of deterministic
or__UUID__
macro that is tied to the file and line__UNIQUE__
Most of these would not be practical, would add boilerplate, or would introduce substantial complexity.
6.3.2. Proposed Solution
This paper proposes no fundamental changes to existing
functionality or language semantics. Instead,
unique identifiers for variables in header-
functions should be solved by:
-
Modules, where
is module-local__COUNTER__ -
The
placeholder [P2169], which is ODR-friendly_
This proposal does not preclude additional functionality or other approaches to make
more ODR-friendly at
a later time.
6.3.2.1. Is __COUNTER__
still needed?
is largely sufficient for uses of
in the case of local identifiers, however, it does not cover
use-cases of
in namespace-scoped identifiers or other preprocessor metaprogramming uses.
As an example of use of
beyond local identifiers google benchmark uses uniquely-named identifiers at
namespace-scope to register benchmark functions:
// after preprocessor expansion: static :: benchmark :: internal :: Benchmark * _benchmark_2FooBar __attribute__ (( unused )) = ( :: benchmark :: internal :: RegisterBenchmarkInternal ( new :: benchmark :: internal :: FunctionBenchmark ( "FooBar" , FooBar ) ) );
An alternative to
in cases such as this would be to standardize
to manage any objects, it is a pure constructor. However, in cases where
an object is managed and possibly needs to be destructed at the end of a program, using a namespace-scoped variable
consolidates the constructor and destructor logic around an object, rather than managing the object between free
functions and a variable. I.e.:
std :: optional < Foo > obj ; __attribute__ (( constructor )) void obj_setup () { obj = setup_foo (); } /* possibly a destructor too */
vs:
Foo obj = setup_foo (); /* or some raii-wrapper around Foo if additional destruction logic is needed beyond normal */
While
covers many uses of
, the preprocessor utility continues to be useful due to existing practice,
uses outside local identifiers, other preprocessor metaprogramming uses of
beyond unique identifiers.
Additionally, because of interest in avoiding divergence in the shared preprocessor, its use in C in an important
consideration.
6.4. Evaluation Order and Caching
Meeting notes for [N3190] mention brief discussion of
making evaluation order and caching observable,
possibly leading to divergence between compiler test cases [N3227].
The evaluation order consideration is similar to existing expression evaluation order observability. Experience from
existing practice has shown this doesn’t pose a substantial concern or footgun. This is especially true given the
typical use for
, which is unique identifiers/indices. When it comes to caching, a conforming
implementation would need to take care to not cache macro expansions involving
.
This paper proposes no change to the current behavior.
6.5. Argument Handling
If passed as an argument to a function-like macro, multiple expansions of an argument including
should
expand to the same token sequence. This paper proposes the following case should produce
. This is the current
behavior on all compilers tested on Compiler Explorer except Chibicc, which produces
.
#define X(Z) Z Z X ( __COUNTER__ ) // 0 0
In the case of an unused argument,
will not be expanded and will not result in an increment. This is the
current behavior for all compilers tested on Compiler Explorer.
#define FOO(X) FOO ( __COUNTER__ ) __COUNTER__ // 0
Additionally in the case of
but no use of
, an expansion of
and subsequent
increment should occur. Currently, Clang diverges from MSVC and GCC in the following example. It produces
while the
others produce
:
#define X(...) __VA_OPT__() X ( __COUNTER__ ) __COUNTER__ // 1
Notably, Clang produces the desired output in the following example:
#define X(...) __VA_OPT__(a) X ( __COUNTER__ ) __COUNTER__ // 1
If used by the stringizing operator or token pasting operator,
should not be incremented. This is the
current behavior on all compilers tested on compiler explorer.
#define STR(X) #X STR ( __COUNTER__ ) // "__COUNTER__" #define CONCAT(X) A##X CONCAT ( __COUNTER__ ) // A__COUNTER__ __COUNTER__ // 0 #define CONCAT2(X) A##X X CONCAT2 ( __COUNTER__ ) // A__COUNTER__ 1 __COUNTER__ // 2
6.6. Range and Overflow
is implemented with an
counter in GCC and Clang and both implementations wrap around to zero
when that counter overflows. This paper recommends
shall be able to attain a value of at
least 232 - 1 with an error on overflow.
7. Proposed Wording
Proposed wording relative to [N4950]:
Insert into [lex.icon]:
decimal - digit - sequence : digit decimal - digit - sequence digit
Insert a bullet point in [cpp.predefined/1] before bullet 3:
__COUNTER__
A decimal-digit-sequence representing the value of a preprocessor-internal counter. The value of the counter starts atand is incremented by
0 each time the
1 macro is expanded. The counter shall have a maximum value of at least 232 - 1. If the value of the counter exceeds its implementation-defined maximum value the program is ill-formed.
__COUNTER__
Update [cpp.predefined/3]:
The values of the predefined macros (except for
__FILE__ and,, and
__LINE__ ) remain constant throughout the translation unit.
__COUNTER__
Update the second bullet in [cpp.subst/1]:
- Otherwise, the replacement preprocessing tokens are the preprocessing tokens of corresponding argument after all macros contained therein have been expanded. This expansion shall be observed to occur no more than once per argument. The argument’s preprocessing tokens are completely macro replaced before being substituted as if they formed the rest of the preprocessing file with no other preprocessing tokens being available.