是否允许比较 static_assert 中静态 class 字段的指针?

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

我试图在 static_assert 中验证程序是否真的有两个不同的 类 通过比较静态字段上的指针从模板生成。经过一些简化后,程序如下所示:

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 没问题,但 GCC 打印错误:

error: non-constant condition for static assertion

演示:https://gcc.godbolt.org/z/o6dE3GaMK

我想知道是否真的允许在编译时进行这种类型的检查?

是的,允许在 static_assert 子句中比较指向静态 class 成员的指针(包括在模板实例化结果中生成的 classes),和指向不同对象的指针(以及不同模板实例化的成员是不同的对象)必须比较不相等(但应该理解它与比较真实的运行时间地址甚至可执行文件中的静态地址,如果可执行文件中有的话(下面有详细信息))。

从问题中发出编译错误是 gcc.

错误的结果

问题作者的确切编译错误的错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102175

相关错误报告:

如果字段明确专门化,代码将适用于 gcc 11.2 with -std=c++20gcc 7.5 with -std=c++17,如下所示:

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); 
}

至于具体要如何,根据C++标准(以下链接为C++20(目前最新接受的语言版本)first post-publication draft, N4868 ):



下面是关于编译时指针的使用与使用真实对象地址无关的细节

在 C++ 中,尽管在转换过程中可以将指向具有静态存储持续时间的对象的指针与另一个此类指针以及与 nullptr/0 和自身进行比较,但它什么都没有在执行时使用内存中对象的真实地址,该功能只是一个存根,它利用了这样一个事实,即每个这样的对象(甚至是空的)都有自己唯一的、非空的地址和关于生命周期的知识对象的。

具有 static storage duration (https://en.cppreference.com/w/cpp/language/storage_duration) 的对象的存储在程序开始时分配并在程序结束时释放,这一事实使得此类对象的地址在程序执行期间永久存在, 人们可能会误以为它使此类对象在编译时获取其地址,但事实并非如此。

虽然在目前最流行的操作系统的二进制可执行文件中,静态存储持续时间的变量地址是静态的,实际上两者都是:

  • 它只是实现的一个变体,并不能保证(C++ 只关注地址在执行中的永久性);
  • 由于 C++ 翻译在实践中对我听说过的所有实现都有效,翻译单元是独立编译的(其他翻译单元的一些变量可能不知道),因此可以分配静态地址仅在编译完成后的链接阶段 (https://en.wikipedia.org/wiki/Linker_(computing))。

不过标准其实是:

  • 不进行编译链接操作,只进行translation;
  • 不认为在另一个翻译单元中分配(定义)的变量真的不能分配(定义)

无论如何编译时,仍然:

  • 已知当前翻译单元中是否存在对象(有内存位置);
  • 显然如果存在静态存储时长的对象,其地址不等于nullptr/0
  • 声明的对象在任何一个翻译单元中都应该是defined/allocated。

因此,标准认为所有此类对象都具有唯一的地址,而不是空地址。



以下是关于gcc 11.2 with -std=c++20gcc 7.5 with -std=c++17中相关案例的实践。


对于 class(不是模板)的静态字段,

即使我们在当前翻译单元中声明它们但没有定义

class D {
public:
    static int d1;
    static int d2;
};

可以将地址与 nullptr/0

进行比较
static_assert(&D::d1 != nullptr);  // compiles successfully
static_assert(&D::d1 != 0);  // compiles successfully

再一次,条件与绝对使用对象地址无关。 即使我们根本没有在任何翻译单元中定义静态字段,编译器仍然会检查条件 true 结果,只有链接器会发出 undefined reference 以防我们尝试 odr-use (https://en.cppreference.com/w/cpp/language/definition#ODR-use) 代码某处的未定义对象。

完全相同的是将静态字段的地址与自身进行比较

static_assert(&D::d1 == &D::d1); // compiles successfully

注,与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`

gcc 7.5 with -std=c++17成功编译,但用gcc 11.2 with -std=c++20编译不成功,它分别对应于C++17,对于这种情况来说,“两个指针都不大于另一个 " (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),gcc 11.2 with -std=c++20,语言版本。


对于比较不同对象地址的编译时条件不同

如果变量未在当前翻译单元中定义(即使它们在另一个翻译单元中),编译器不接受条件作为有效 constant expression

static_assert(&D::d1 != &D::d2); // error: ‘((& D::d1) != (& D::d2))’ is not a constant expression

请注意,尽管从编译器不知道对象是否真正定义(分配)在任何其他翻译单元中的角度来看是有道理的,它违反了standard, 不需要指针指向定义的对象进行比较;

如果在当前翻译单元中有定义,则条件编译成功

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

至于模板,

如果我们尝试比较 class 模板本身的特化对象的地址,结果(除了下面的注释)与模板静态字段的特化相同class 问题模板。

注意,根据标准,reinterpret_cast 防止表达式成为 constant expression (https://timsong-cpp.github.io/cppwp/n4868/expr.const#5.15),因此 gcc 11.2 with -std=c++20,以及 clang 13.0.0 with -std=c++20msvc v19.29 VS16.11 with /std:c++17 ,拒绝编译 constant expression 中的 reinterpret_cast,但 gcc 7.5 with -std=c++17 确实接受它,所以让我们尝试比较 static_assert 子句中的地址gcc 7.5 with -std=c++17.

所以,对于下面的代码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

编译器不接受比较地址子句,

但如果模板是明确特化的

C<0> c0;
C<1> c1;

我们可以在编译时比较地址(对于gcc 7.5 with -std=c++17

static_assert(reinterpret_cast<const void *>(&c0) != reinterpret_cast<const void *>(&c1)); //successfully compiled

需要注意的是

template<> int C<0>::x = 0;
template<> int C<1>::x = 0;

从答案的第一个代码块和

C<0> c0;
C<1> c1;

是变量的定义,它在同一个翻译单元中创建对象。

确实如此

template<int N> int C<N>::x = 0;

template<int N> C<N> cI;

实际上对 gcc 11.2 with -std=c++20gcc 7.5 with -std=c++17 做同样的事情?

检查如下。如果我涉及一个额外的源文件,通过模板特化创建对象,如下所示

template<int N> struct C { 
    static int x; 
};

template<> int C<0>::x = 0;
template<> int C<1>::x = 0;

并在 main.cpp 中保留 class 模板的非模板数据成员的定义,并按如下方式引入隐式实例化:

#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";
}

我明白了

multiple definition of `C<0>::x'

multiple definition of `C<1>::x'

链接器错误,

这意味着对象实际上也是从 template<int N> int C<N>::x = 0; 创建的。

如果我从附加文件中删除定义,所有这些都会成功编译,我可以在 运行 时间输出中看到地址。

但是如果我加上

static_assert(&C<0>::x != &C<1>::x); // error: '((& C<0>::x) != (& C<1>::x))' is not a constant expression

之后

std::cout << &C<0>::x << &C<1>::x << "\n";

它仍然不被接受(尽管对象肯定存在)。

所以,当我们使用

template<int N> int C<N>::x = 0;

变量是实际创建的,但它们的地址在编译时不可比较(对于gcc 11.2 with -std=c++20gcc 7.5 with -std=c++17)。


将等待问题作者提交的错误报告 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102175 的回复。

这应该是评论而不是答案,但它不适合评论部分的可读方式。

所以关于问题的意图:

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

您的代码片段是 class:

的模板
template<int N> struct C { 
    static int x; 
};

这意味着它本身不是 class,而是一个蓝图。实际的 class 是 C<0>C<1>。如果为 N 选择的值不同,则结果 class 也不同。

要弄清楚两种类型是否不同,您可以使用(正如您在评论中正确提到的那样)std::is_same_v:

static_assert( !std::is_same_v<C<0>,C<1>>);

无论 static_assert(&C<0>::x != &C<1>::x); 是否有效,您都不应该使用它来确定这些类型是否与语言的角度不同。在最坏的情况下,您会 - 由于请求成员的内存地址 - 阻止编译器进行一些它之前可以进行的优化。


现在你可以说编译器可以根据 as-if 进行优化并在这些类型之间共享部分,因此它们在二进制级别上没有区别(语言上的区别和二进制上的区别是两个不同的东西)。

但是由于相同的 as-if 规则,无法在代码中真正测试它们在二进制级别上的不同程度。

让我们暂时假设 static_assert(&C<0>::x != &C<1>::x); 可以工作,然后 &C<0>::x != &C<1>::x 必须评估为 true,因为 C<0>C<1> 是不同的类型。这可能由于两件事而发生:

  1. 因为您要求 x 用于 C<0>C<1> 的内存地址,您实际上阻止了编译器进行优化,之后它们将共享 x在二进制层面上,所以二进制层面上的内存地址其实是不一样的

  2. 编译器仍然进行优化,以便 xC<0>C<1> 之间共享。但是 &C<0>::x != &C<1>::x 的结果仍然是 true 因为根据 as-if 规则它必须是 true。因此,即使 x 两种不同类型 C<0>C<1> 的地址在二进制级别上相同,static_assert 测试也将是 true.

允许对静态 class 成员的指针进行编译时相等性比较。

原始代码的问题仅出现在 GCC 中,并且由于 old GCC bug 85428

要解决此问题,可以按如下方式显式实例化模板:

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); 
}

此程序现已被所有 3 个主要编译器接受,演示:https://gcc.godbolt.org/z/Kss3x8GnW