我尝试根据 C++23 规范确定如下代码是否合法:

#include <type_traits>

template<bool B> struct S {
  int x;

  void go() {
    ++x;
    if constexpr (B)
      reinterpret_cast<S<false>*>(this)->go();
  }
};
static_assert(std::is_standard_layout_v<S<true>>
          && std::is_standard_layout_v<S<false>>);

int
main()
{
  S<true>{}.go();
  return 0;
}

指出S<true>与 布局兼容S<false>

此外,我认为意味着S<true>通过传递性可以进行指针互换,因为S<false>可以(因为)S<true>进行指针互换,并且根据同样的推理,可以与进行指针互换intS<true>::xintS<false>

另一方面,我不明白如何允许这样做,因为S<true>S<false>没有相同的动态类型并且不(相似性意味着在 cv 限定符和数组到指针分解等方面相同)。

我是否遗漏了有关严格别名规则的某些内容?如果我无法reinterpret_cast在指向布局兼容类型的指针之间进行操作,那么布局兼容性的意义何在?如果我放入x模板的非模板化超类型(不留下模板的非静态成员,因此它仍然是标准布局),我的示例是否合法?

4

  • 据我所知,不,这是不合法的。“布局兼容”和“指针可相互转换”都不意味着您可以为此类对象设置别名。指针可相互转换意味着您可以获取类型 U 的指针,指向类型 T 的对象,但不允许取消引用该指针U*。例如,您可以存储它(U*)并将其转换回原始的T*


    – 

  • 真倒霉。事实上,这在实践中是可行的,这意味着编译器没有利用 UB 进行优化,所以我们只能两全其美… 我不能合法地做一些方便的事情,但编译器也不知道如何利用我的不便来进行优化。


    – 

  • 3
    由于您的代码是 UB,因此您无法真正看到编译器是否在此基础上进行了优化。抗锯齿是一种非常强大的优化。请看此处,一个与您的模板类似的示例:因为S<true>S<false>不能互为别名,所以编译器知道s2->x = 11不能写入s1->x,因此return s1->x;它不会读取内存,而只是读取mov eax, 24


    – 


  • 我觉得这个问题很有趣。显然已经研究得足够多了,以至于寻求指导是正确的做法。+1


    – 


最佳答案
1

[class.mem.general] 指出 S 与 S 布局兼容。

布局兼容性与严格别名规则无关。它纯粹用于确定在某些特殊情况下是否允许通过联合的非活动成员进行访问(见下文),而严格别名规则是关于通过与对象类型不同的泛左值表达式来访问对象。

此外,我认为 [basic.compound] 意味着S<true>S<false>通过传递性可以进行指针互换,因为可以与(因为)S<true>进行指针互换,并且根据同样的推理,可以与进行指针互换intS<true>::xintS<false>

指针不可转换属性是在对象之间定义的,而不是类型之间。为了reinterpret_cast<S<false>*>(this)能够产生指向对象的指针,首先必须在地址处S<false>存在一个对象。然后,此外,该对象必须与引用的对象可进行指针互转换。您的示例中不存在任何对象,因此考虑指针互转换是没有意义的。S<false>this*thisS<false>

另一方面,我不明白严格的别名规则如何允许这样做,因为S<true>S<false>没有相同的动态类型并且不相似(相似性意味着在 cv 限定符和数组到指针分解等方面相同)。

仅当指针相互转换未导致reinterpret_cast生成指向正确类型的对象的指针时,严格别名规则才有意义。reinterpret_cast在这种情况下将生成指向原始对象的指针,但reinterpret_cast请求强制转换为的表达式类型不匹配(假设指针与目标类型适当对齐,否则结果未指定)。

严格别名规则指定在哪种情况下允许仍然通过reinterpret_cast最后一种情况下的结果进行访问,即当结果是T*指向类型为 的对象U而不是 类型的对象的类型指针时T。 如果指针可相互转换导致它导致T*指向类型为 的对象的类型指针T,则严格别名规则不相关。

中的严格别名规则仅与访问有关,这是一个仅适用于标量对象的读写的定义术语。该规则与类类型之间永远没有相关性(与 C 中的等效规则相反)。相反,采用类类型操作数的表达式对所使用的 glvalue 表达式是否必须引用与表达式类型匹配的对象有自己的限制。([basic.lval]/11 还包括一个针对联合的默认复制/移动构造函数的此类附加要求)

例如,使用.(或间接->) 进行非静态成员访问需要左侧实际引用一个类型类似于自身规则中的表达式类型的对象 (请参阅 )。这正是导致您的成员访问表达式reinterpret_cast<S<false>*>(this)->go具有未定义行为的规则:this指向一个对象。在同一地址S<true>不存在 (指针可相互转换的)对象,因此引用类型对象的类型 glvalue 表达式也是如此,但并不相似。S<false>*reinterpret_cast<S<false>*>(this)S<false>S<true>S<false>S<true>

如果我不能在指向布局兼容类型的指针之间重新解释_转换,那么布局兼容性的意义何在?

布局兼容性用于一种特定情况,允许读取(但不能修改!)联合的非活动成员的成员,就好像它们引用了活动成员的相应成员一样,前提是联合成员是具有通用的布局兼容成员初始序列的标准布局类。这是该属性在标准语言中的唯一相关性。例如:

union U {
    S<true> a;
    S<false> b;
};

int main() {
    U u;
    u.a = {1};

    // Allowed because `S<true>` and `S<false>` are standard-layout and
    // share a common initial sequence of members that are layout-compatible and
    // `x` is part of that initial sequence.
    return u.b.x; 
}

0