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):
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:
- it is only a variant of implementation and is not guaranteed (C++ concerns only of the addresses being permanent within an execution);
- as C++ translation really works in practice for all implementations I ever heard about, translation units are compiled independently (some of the variables from other translation units just may not be known), so the static addresses are possible to be assigned only at linking stage (https://en.wikipedia.org/wiki/Linker_(computing)), which is after compilation has been completed.
But though the standard factually:
- does not operate compiling and linking but only
translation;
- does not consider that a variable intended to be allocated (defined) in another translation unit can really be not allocated (defined)
when compiling in any case, nevertheless:
- it is known whether or not an object exists (has memory location) in the current translation unit;
- it is obvious that if the object with static storage duration exists, its address is not equal to
nullptr/0;
- it is expected that a declared object is defined/allocated in any one translation unit.
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.