Reputation: 21307
I was trying to verify in a static_assert
that a program had really two distinct classes produced from a template by comparing the pointers on their static fields. After some simplifications the program looks as follows:
template<int N> struct C {
static int x;
};
template<int N> int C<N>::x = 0;
int main() {
static_assert(&C<0>::x != &C<1>::x);
}
Clang is ok with it, but GCC prints the error:
error: non-constant condition for static assertion
demo: https://gcc.godbolt.org/z/o6dE3GaMK
I wonder whether this type of check is really allowed to do in compile time?
Upvotes: 7
Views: 869
Reputation: 677
Yes, comparison of pointers to static class members (including for classes generated in result of template instantiations) in a static_assert
clause is allowed, and pointers to different objects (and the members of the different template instantiations are different objects) must compare unequal (but one should understand that it has to do with comparing neither real run-time addresses nor even static addresses in an executable, if any in the executable (there are details of it below)).
Issuing the compilation error from the question is a result of a bug of gcc
.
The bug report for the exact compilation error by the author of the question: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102175
The related bugs reports:
The code will work for gcc 11.2 with -std=c++20
and gcc 7.5 with -std=c++17
if the fields are explicitly specialized, like as follows:
template<int N> struct C {
static int x;
};
template<int N> int C<N>::x = 0;
template<> int C<0>::x = 0;
template<> int C<1>::x = 0;
int main() {
static_assert(&C<0>::x != &C<1>::x);
}
As for specifically how it must be, according to the C++ standard (the following links are for the C++20 (the currently latest accepted version of the language) first post-publication draft, N4868):
static_assert
clause (https://timsong-cpp.github.io/cppwp/n4868/expr.const);The following is in regards to the details of the fact that the usage of pointers at compile-time has nothing to do with using real objects addresses.
In C++, though during translation a pointer to an object with static storage duration can be compared with another such pointer, as well as with nullptr
/0
and with itself, it has nothing to do with using real addresses of the objects in memory while an execution, and the feature is just a stub, which utilizes the fact that every such object (even empty) has its own unique, non-null address and knowledge about life-time of the object.
The fact that the storage for the object with static storage duration
(https://en.cppreference.com/w/cpp/language/storage_duration) is allocated when the program begins and deallocated when the program ends makes addresses of such objects permanent during an execution of a program, and one may be misled that it makes such objects get their addresses at compile time, but it is not so.
Though in binary executables for most popular at present operating systems, addresses of variables with static storage duration are static, actually both:
But though the standard factually:
translation
;when compiling in any case, nevertheless:
nullptr
/0
;So, the standard considers all such objects to have unique, not null addresses.
The following is in regards to the related cases in gcc 11.2 with -std=c++20
and gcc 7.5 with -std=c++17
in practice.
For static fields of a class (not template),
even if we declare them in the current translation unit but not define
class D {
public:
static int d1;
static int d2;
};
it is possible to compare an address with nullptr
/0
static_assert(&D::d1 != nullptr); // compiles successfully
static_assert(&D::d1 != 0); // compiles successfully
One more time, the conditions have nothing to do with using the address of an object absolutely.
Even if we don’t define the static fields in any translation unit at all, the compiler checks the conditions with true
result anyway, and only the linker will issue an undefined reference
in case we try to odr-use
(https://en.cppreference.com/w/cpp/language/definition#ODR-use) the undefined object somewhere in the code.
Exactly the same is for comparing an address of a static field with itself
static_assert(&D::d1 == &D::d1); // compiles successfully
Note, the relation comparing with nullptr
/0
static_assert(&D::d1 > nullptr); // compiles successfully with `gcc 7.5 with -std=c++17` but NOT with `gcc 11.2 with -std=c++20`
static_assert(&D::d1 > 0); // compiles successfully with `gcc 7.5 with -std=c++17` but NOT with `gcc 11.2 with -std=c++20`
successfully compiles with gcc 7.5 with -std=c++17
, but not with gcc 11.2 with -std=c++20
, and it respectively corresponds to C++17
, which for the cases says "neither pointer compares greater than the other" (https://timsong-cpp.github.io/cppwp/n4659/expr.rel#3.3), for gcc 7.5 with -std=c++17
, and C++20
, which for the case says "neither pointer is required to compare greater than the other" (https://timsong-cpp.github.io/cppwp/n4868/expr.rel#4.3), for gcc 11.2 with -std=c++20
, language versions.
For a compile-time condition for comparing addresses of different objects it differs.
If the variables are not defined in the current translation unit (even if they are in another one), the compiler doesn't accept the condition as a valid constant expression
.
static_assert(&D::d1 != &D::d2); // error: ‘((& D::d1) != (& D::d2))’ is not a constant expression
Note, though it make sense from the point of view that the compiler does not know whether or not the object was really defined (allocated) in any one of the other translation units, it violates the standard, which does not require the pointers point to defined objects for the comparing;
if they are defined in the current translation unit, the condition is compiled successfully.
class D {
public:
static int d1;
static int d2;
};
int D::d1 = 0;
int D::d2 = 0;
static_assert(&D::d1 == &D::d1); // compiles successfully
As for templates,
if we try to compare addresses of the objects of specializations of the class template themselves, the result (except as in the note right below) is the same as for the specializations of the static fields of the class template from the question.
Note, according to the standard, reinterpret_cast
prevents an expression from being a constant expression
(https://timsong-cpp.github.io/cppwp/n4868/expr.const#5.15), so gcc 11.2 with -std=c++20
, as well as clang 13.0.0 with -std=c++20
and msvc v19.29 VS16.11 with /std:c++17
, refuses to compile reinterpret_cast
in the constant expression
, but gcc 7.5 with -std=c++17
does accept it, so lets try to compare the addresses in a static_assert
clause with gcc 7.5 with -std=c++17
.
So, for the following code for gcc 7.5 with -std=c++17
template<int N> struct C {
static int x;
};
template<int N> C<N> cI;
static_assert(reinterpret_cast<const void *>(&cI<0>) != reinterpret_cast<const void *>(&cI<1>)); //error: ‘(((const void*)(& cI<0>)) != ((const void*)(& cI<1>)))’ is not a constant expression
comparing addresses clause is not accepted by the compiler,
but if the template is explicitly specialized
C<0> c0;
C<1> c1;
we can compare the addresses at compile-time (for gcc 7.5 with -std=c++17
)
static_assert(reinterpret_cast<const void *>(&c0) != reinterpret_cast<const void *>(&c1)); //successfully compiled
It should be noted that
template<> int C<0>::x = 0;
template<> int C<1>::x = 0;
from the first code block of the answer and
C<0> c0;
C<1> c1;
are definitions of the variables, which create objects in the same translation unit.
And does
template<int N> int C<N>::x = 0;
and
template<int N> C<N> cI;
factually do the same with gcc 11.2 with -std=c++20
and gcc 7.5 with -std=c++17
?
The check is as follows. If I involve an additional source file with creation of the objects by template specializations as follows
template<int N> struct C {
static int x;
};
template<> int C<0>::x = 0;
template<> int C<1>::x = 0;
and keep in the main.cpp
the definition of a non-template data member of a class template and induce implicit instantiation as follows:
#include <iostream>
template<int N> struct C {
static int x;
};
template<int N> int C<N>::x = 0;
int main() {
std::cout << &C<0>::x << &C<1>::x << "\n";
}
I get
multiple definition of `C<0>::x'
multiple definition of `C<1>::x'
linker errors,
which means the objects are really created from template<int N> int C<N>::x = 0;
too.
If I remove definitions from the additional file, all this compiles successfully and I can see the addresses in the run-time output.
But if I add
static_assert(&C<0>::x != &C<1>::x); // error: '((& C<0>::x) != (& C<1>::x))' is not a constant expression
after
std::cout << &C<0>::x << &C<1>::x << "\n";
it still is not accepted (though the objects are guaranteedly exist).
So, when we use
template<int N> int C<N>::x = 0;
the variables are factually created but their addresses are not comparable at compile time (for gcc 11.2 with -std=c++20
and gcc 7.5 with -std=c++17
).
Will wait for a response to the bug report https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102175 submitted by the author of the question.
Upvotes: -1
Reputation: 21307
Compile-time equality comparison of the pointers on static class members is allowed.
The problem with original code was only in GCC and due to old GCC bug 85428.
To workaround it, one can explicitly instantiate the templates as follows:
template<int N> struct C {
static int x;
};
template<int N> int C<N>::x = 0;
template struct C<0>;
template struct C<1>;
int main() {
static_assert(&C<0>::x != &C<1>::x);
}
This program is now accepted by all 3 major compilers, demo: https://gcc.godbolt.org/z/Kss3x8GnW
Upvotes: 2
Reputation: 40872
This should be more comment than an answer, but it does not fit in a readable way in the comments section.
So regarding the intent of the question:
I was trying to verify in a
static_assert
that a program had really two distinct classes produced from a template by comparing the pointers on their static fields
Your code snippet is a template for a class:
template<int N> struct C {
static int x;
};
This means that it is not a class by itself but a blueprint. The actual classes are C<0>
and C<1>
. If the values chosen for N
are distinct then the resulting classes are distinct.
To figure out if two types are distinct you would use (as you correctly mention in your comment) std::is_same_v
:
static_assert( !std::is_same_v<C<0>,C<1>>);
No matter if static_assert(&C<0>::x != &C<1>::x);
is valid or not you shouldn't use it to figure out if those types are distinct from the perspective of the language. In the worst case you would - due to requesting the memory address of a member - prevent the compiler to do some optimizations it could before.
Now you could say the compiler can do optimizations based on the as-if and share parts between those types, so they are not distinct on the binary level (distinct on the language and distinct in the binary are two different things).
But to which degree those are distinct on the binary level cannot be really tested within the code due to the same as-if rules.
Let's assume for a moment that static_assert(&C<0>::x != &C<1>::x);
would work, then &C<0>::x != &C<1>::x
has to evaluate to true
, because the C<0>
and C<1>
are distinct type. This could happen due to two things:
Because you ask for the memory address of x
for C<0>
and C<1>
you actually prevent the compiler to do an optimization after which they would share x
on the binary level, so the memory address on the binary level is actually different.
The compiler still does the optimization so that x
is shared between C<0>
and C<1>
. But the outcome of &C<0>::x != &C<1>::x
would still be true
because it has to be true
according to the as-if rule. So even if the address of x
for both distinct types C<0>
and C<1>
would be the same on the binary level the static_assert
test would be true
.
Upvotes: 1