Date: Sun, 4 Feb 2024 13:10:29 +0100
I have a question about the desirability of stateful metaprogramming in
general. From CWG 2118 ("Stateful metaprogramming via friend injection") I
made a conclusion that it is not something we would like to have, but the
paragraph 3.15 of P2996 demonstrates how this proposal makes it easier to
implement a compile-time counter.
I'm worried about this. Firstly, because if the standard mandates that the
compiler state is visible somehow to the user, then it should specify
observed behavior. But it's hard due to the freedom compilers have in some
areas. The typical example is the point of instantiation of template
function. Let's consider the following program (
https://godbolt.org/z/4aajT1fvj):
template<typename>
struct TU_Ticket {
static consteval int next() {
...
}
};
template<typename T>
int f() {
return TU_Ticket<T>::next();
}
template<typename T>
int f() {
return TU_Ticket<T>::next();
}
int g();
int x = f<void>(); // #1
int y = g(); // #2
int g() { // #3
return TU_Ticket<void>::next();
}
int main() {
std::cout << "x=" << x << ", y=" << y << '\n';
}
The output produced by EDG is "x=1, y=0" and it demonstrates the fact that
the definition of 'f<void>' is instantiated below call of `f<void>()` (#1)
and call of `g()` (#2) and even the definition of `g` (#3).
The standard cannot specify it, because we don't want to define strictly at
which point the definition of template function is instantiated. So the
behavior of this program will be unspecified? Is it fine?
The second reason why I don't like to make the compiler state observable by
the user is that it causes problems with any kind of lazy or partial
parsing. For instance, fast module interface parsing, or body-skipping in
an IDE-centric parser, and so on.
Let's consider the following source file:
struct TU_Ticket {
static consteval int next() {
...
}
};
void a() {
TU_Ticket::next();
}
std::integral_constant<int, TU_Ticket::next()> b();
Note that function 'a' is non-inline, non-constexpr, doesn't have a deduced
return type. It means that the definition of 'a' doesn't affect the
"interface" of the file. (The term "interface" could be interpreted in
different senses, like "C++20 module interface" or "set of symbols which
are declared in the file and should be processed by IDE or some indexing
tool like clangd".)
So it's tempting for lazy parsers to skip the body of 'a'. But it leads to
evaluation of the return type of function 'b' to 'integral_constant<int,
0>' instead of 'integral_constant<int, 1>' like a normal compiler will do.
Also, I see the similar issue with proposed `std::meta::define_class`.
Without doubt, it's handy functionality, but it modifies the compiler
state, and the proposal doesn't define or limit somehow the context where
this function could be called. So I'm afraid it will be called in some
contexts which are usually skipped by lazy/body-skipping parsers.
general. From CWG 2118 ("Stateful metaprogramming via friend injection") I
made a conclusion that it is not something we would like to have, but the
paragraph 3.15 of P2996 demonstrates how this proposal makes it easier to
implement a compile-time counter.
I'm worried about this. Firstly, because if the standard mandates that the
compiler state is visible somehow to the user, then it should specify
observed behavior. But it's hard due to the freedom compilers have in some
areas. The typical example is the point of instantiation of template
function. Let's consider the following program (
https://godbolt.org/z/4aajT1fvj):
template<typename>
struct TU_Ticket {
static consteval int next() {
...
}
};
template<typename T>
int f() {
return TU_Ticket<T>::next();
}
template<typename T>
int f() {
return TU_Ticket<T>::next();
}
int g();
int x = f<void>(); // #1
int y = g(); // #2
int g() { // #3
return TU_Ticket<void>::next();
}
int main() {
std::cout << "x=" << x << ", y=" << y << '\n';
}
The output produced by EDG is "x=1, y=0" and it demonstrates the fact that
the definition of 'f<void>' is instantiated below call of `f<void>()` (#1)
and call of `g()` (#2) and even the definition of `g` (#3).
The standard cannot specify it, because we don't want to define strictly at
which point the definition of template function is instantiated. So the
behavior of this program will be unspecified? Is it fine?
The second reason why I don't like to make the compiler state observable by
the user is that it causes problems with any kind of lazy or partial
parsing. For instance, fast module interface parsing, or body-skipping in
an IDE-centric parser, and so on.
Let's consider the following source file:
struct TU_Ticket {
static consteval int next() {
...
}
};
void a() {
TU_Ticket::next();
}
std::integral_constant<int, TU_Ticket::next()> b();
Note that function 'a' is non-inline, non-constexpr, doesn't have a deduced
return type. It means that the definition of 'a' doesn't affect the
"interface" of the file. (The term "interface" could be interpreted in
different senses, like "C++20 module interface" or "set of symbols which
are declared in the file and should be processed by IDE or some indexing
tool like clangd".)
So it's tempting for lazy parsers to skip the body of 'a'. But it leads to
evaluation of the return type of function 'b' to 'integral_constant<int,
0>' instead of 'integral_constant<int, 1>' like a normal compiler will do.
Also, I see the similar issue with proposed `std::meta::define_class`.
Without doubt, it's handy functionality, but it modifies the compiler
state, and the proposal doesn't define or limit somehow the context where
this function could be called. So I'm afraid it will be called in some
contexts which are usually skipped by lazy/body-skipping parsers.
-- Andrey Davydov
Received on 2024-02-04 12:10:42