Fedor
Fedor

Reputation: 21307

Is it allowed comparing the pointers on static class fields in static_assert?

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

Answers (3)

Arthur Golubev 1985
Arthur Golubev 1985

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):



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.

Upvotes: -1

Fedor
Fedor

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

t.niese
t.niese

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:

  1. 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.

  2. 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

Related Questions