It is proposed to develop a safe alternative to the built-in macro
__COUNTER__
. The first macro entry is replaced with
0
, the second with
1
, and so on. The value
__COUNTER__
substituted at the preprocessing stage, therefore it can be used in the context of a
constant expression .
Unfortunately, the macro
__COUNTER__
dangerous to use in header files - with a different order of inclusion of header files, the substituted counter values ​​will change. This can lead to a situation where, for example, in
foo.cpp
value of the
AWESOME
constant is 42, while in
bar.cpp
AWESOME≡33
. This is a violation of the principle of one definition rule, that there is a terrible crime in the C ++ universe.
You need the ability to use local counters instead of a single global (at least for each header file its own). In this case, the ability to use the value of the counter in a
constant expression should be preserved.
')
Based on
this question on Stack Overflow.
Motivating example
When is the compile time counter useful?Suppose we want to implement macros to define simple data structures with serialization support. It might look something like this:
STRUCT(Point3D) FIELD(x, float) FIELD(y, float) FIELD(z, float) END_STRUCT
Here we do not just define a
Point3D
structure with a list of
x, y
and
z
fields. We also
automatically get the serialization and deserialization functions. It is impossible to add a new field and forget serialization support for it. You have to write much less than for example for
boost .
Unfortunately, we need to go through the list of fields at least two times: to create field definitions and to generate the serialization function. With the help of the preprocessor alone, this cannot be done. But as you know, any problem in C ++ can be solved with the help of templates (except for the problem of oversupply of templates).
Define the
FIELD
macro as follows (for clarity, use
__COUNTER__
):
#define FIELD(name, type) \ type name;
When expanding
FIELD(x, float)
will turn out
float x;
When expanding
FIELD(y, float)
turns out
float y;
Each subsequent occurrence of the
FIELD()
macro expands to the field definition,
plus the specialization of the function
serialize<
i >()
where
i = 0,1,2, ...
N. The function
serialize<i>()
calls
serialize<i+1>()
, and so on. The counter helps to connect the disparate functions together.
Under the
link a working code sample.
One-bit Compile Time Counter
To begin with, we will show implementation of the one-bit counter.
- We define the template structure
cn<n>
. Note that sizeof(cn<n>) ≡ n+1
.
- We define the template
magic
function.
- The
sizeof
operator applied to an expression returns the size of the type that this expression has. Since the expression is not evaluated, the body definition of the magic
function is not required.
The only currently defined magic
function is a template from p. 2. Therefore, the type of the return value and the entire expression is cn<0>
.
- Define the overloaded
magic
function. Note that ambiguities do not occur when magic
called, because overloaded functions take precedence over template functions.
- Now, when calling
magic(cn<0>())
, another version of the function will be used; expression type inside sizeof
- cn<
1 >()
.
Summarizing - we have an expression with a function call. Add the definition of the overloaded function, as a result, the compiler uses a new function. Thus, the type of the return value from the function and the type of the whole expression changed, although the
textual expression remained the same.
Define macros to read and “increment” a one-bit counter.
#define counter_read(id) \ (sizeof(magic(cn<0>())) - 1) #define counter_inc(id) \ cn<1> magic(cn<0>)
about id parameterAccording to the plan, the id parameter allows you to use several independent counters, by defining unique types and using them as id . For id support, the magic
function must take the additional id
parameter. The overloaded magic
functions will be related to a specific id , and will not affect all other id's .
N-bit compile time counter
N-bit counter is based on the same principles as one-bit. Instead of one
magic
call inside
sizeof
, we will have a chain of nested calls
a(b(c(d(e( … ))))).

Here it is, our basic building block. This is a function of a single argument of type T
0 . Depending on the available declarations in scope, the return type is either T
0 or T
1 . This device resembles an arrow on the rail. In the initial state, the "arrow" is directed to the left. "Arrow" can be switched only once.
Using several basic blocks, we can build an extensive network:

When searching for a suitable variant of a function, the C ++ compiler takes into account only the types of parameters and ignores the type of the returned value. If the expression has nested function calls, the compiler “moves” from the inside to the outside. For example, in the following expression: M
1 (M
2 (M
4 (T
0 ()))), the compiler first allows (“resolves”) the call to the function M
4 (T
0 ). Then, depending on the type of return value of the function M
4 , it allows the call M
2 (T
0 ) or M
2 (T
4 ), and so on.
Continuing the railway analogy, we can say that the compiler moves from top to bottom along the railway network, “turning” on the arrows to the right or left. An expression of
N nested function calls generates a network with 2
N outputs. By switching the arrows in the correct order, you can consistently get all 2
N possible types T
i at the output of the network.
It can be shown that if the current type is at the output of the network T
i , then the next one needs to switch the arrow
M [(i + 1) & ~ i, (i + 1) & i] .
The final version of the code is available
here .
Instead of conclusion
The compile time counter is entirely based on the overloaded functions mechanism. This technique I
spied on Stack Overflow. As a rule,
non-trivial compile-time calculations in C ++ are implemented on templates, which is why the presented solution is especially interesting, since instead of templates it exploits other mechanisms.
How practical are these solutions?
IMHO if a single C ++ file is compiled for more than 5 minutes, and only the latest version of the compiler can handle it - this is definitely
impractical . Many "creative" uses of language features in C ++ are of academic interest only. As a rule, the same tasks can be better solved in other ways, for example, by using an external code generator. Although, I must say, the author is somewhat biased in this matter, categorically not recognizing the
spirit , and experiencing some weakness in relation to
bison .
It seems that the compile time counter is also not very practical, as can be clearly seen in the following graph. The
x- axis represents the absolute value of the increment of the counter in the test program (the test program consists of the lines
counter_inc(int)
), the
y -axis - the compilation time in seconds. For comparison, the compilation time of nginx-1.5.2 is also postponed there.
