C++中的严格别名规则和类型别名

9
我正在尝试理解违反严格别名规则时的未定义行为。我已经阅读了许多关于此问题的文章,但仍有一个问题:我不太明白两种类型何时非法别名。cpp-reference指出:

类型别名

每当试图通过类型为AliasedType的glvalue读取或修改DynamicType类型对象的存储值时,除非以下情况之一成立,否则行为是未定义的:

  • AliasedType和DynamicType相似。
  • AliasedType是DynamicType的(可能是cv-qualified的)有符号或无符号变体。
  • AliasedType是std::byte、(自C++17起)char或unsigned char:这允许将任何对象的对象表示视为字节数组进行检查。
我还在SO上找到了一个很好的例子,其中我清楚地看到了问题:
int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

"

intfloat是不同类型的数据,这个程序可能会造成严重后果。我看不懂也不理解以下修改的意义:

"
struct A
{
    int a;
};

struct B
{
    int b;
};

A foo( A *a, B *b ) { 
    a->a = 1;               
    b->b = 0;            

    return *a;
}

int main() {
    A a;
    a.a = 0;


    std::cout << a.a << "\n";   // Expect 0
    a = foo(&a, reinterpret_cast<B*>(&a));
    std::cout << a.a << "\n";   // Expect 0?
}

我想知道 AB 是否属于相似类型,一切是否正常,还是它们在非法别名使用,导致未定义的行为。如果这是合法的,是因为 AB 是聚合体(如果是,我需要做出什么改变才能使其成为未定义的行为)?

非常感谢您提供任何提示和帮助。

编辑 关于重复问题

我知道 this 帖子,但我没有看到他们澄清哪些类型是相似的。至少我不理解。因此,如果您不关闭此问题,那就太好了。


可能是reinterpret_cast vs strict aliasing的重复问题。 - Richard Critten
2
"A"和"B"是不相关的类型,这样做是非法的。 - bolov
2
"...A 和 B 是兼容类型吗?":不是。" - Richard Critten
1
@RichardCritten 我不明白为什么你把这个问题标记为重复,因为另一个问题并没有真正尝试澄清两种类型何时兼容,这是这个问题的主要问题。我又错过了重要的东西吗? - user4290866
@RichardCritten,您能否更清楚地解释一下您的“不”? - user4290866
1
C类型的文档引用了“兼容类型”,链接为 https://en.cppreference.com/w/c/language/type ;而C++文档则没有,链接为 https://en.cppreference.com/w/cpp/language/type 。因此,“兼容类型”对于C++不相关,而重复的链接解释了其他所有内容。在对象是什么以及如何操作/引用它的领域中,CC++之间存在显着差异。混合使用两个标准的术语可能会导致混淆。 - Richard Critten
3个回答

2
不,这是不合法的,你会有未定义行为:
8.2.1 值类别 [basic.lval] 11 如果程序试图通过非以下类型之一的glvalue访问对象的存储值,则行为是未定义的:63 (11.1) — 对象的动态类型, (11.2) — 对象的动态类型的cv限定版本, (11.3) — 类型与对象的动态类型相似(如7.5中定义), (11.4) — 是与对象的动态类型对应的有符号或无符号类型, (11.5) — 是与对象的动态类型的cv限定版本对应的有符号或无符号类型, (11.6) — 包括上述类型之一作为其元素或非静态数据成员(包括递归地是子聚合或包含联合的元素或非静态数据成员)的聚合体或联合体, (11.7) — 是对象的动态类型的(可能带有cv限定)基类类型, (11.8) — char、unsigned char或std::byte类型
63)此列表的目的是指定对象可能或可能不被别名的情况。

1
在表达式 b->b = a; 中,未定义的行为不是由于赋值,而是由于类成员访问表达式b->b。如果这个表达式不是未定义的行为,你的代码就不会是未定义的行为。
[expr.ref]/1中指定了类成员访问构成对对象b的访问(在->的左侧):
引用块中的后缀表达式后跟一个点.或箭头->,可选地跟随关键字template([temp.names]),然后跟随id-expression,是一个后缀表达式。在点或箭头之前的后缀表达式被评估;该评估的结果连同id-expression确定整个后缀表达式的结果。
加粗是我的。
所以,b->b 使用类型为B 的表达式读取对象a 的值,并且你引用的规则在此适用。

给定类似于 intPtr1 = &someVolatileStructArray[i++].someIntMember; 的语句,标准如何描述子表达式 someVolatileStructArray[i++] 的操作?由于该语句会增加 i,但不执行任何 volatile 访问,我认为很明显代码对该子表达式进行了某些操作,但并没有对其进行评估。 - supercat
@supercat 如果 someVolatileStructArray[i++],我在标准中没有找到任何明确的说明。我的推理是,在 [exp.ref] 规则中并没有说明对虚基类成员或虚函数成员的访问,或者在这种情况下是否适用于 volatile。因此,为了使语言一致,我认为在 [exp.ref]/1 中,“评估”也意味着访问。 - Oliv

1

关于类似类型,reinterpret_cast 部分有一些有用的解释和示例:

非正式地,如果忽略顶层cv限定符,则两种类型在以下情况下相似:
它们是相同的类型;或
它们都是指针,并且所指向的类型是相似的;或
它们都是指向同一类的成员的指针,并且所指向的成员的类型是相似的;或
它们都是相同大小的数组或未知边界的数组,并且数组元素类型是相似的。
例如:
const int * volatile *和int ** const是相似的;
const int(* volatile S::* const)[20]和int(* const S::* volatile)[20]是相似的;
int(* const *)(int *)和int(* volatile *)(int *)是相似的;
int(S::*)() const和int(S::*)()不相似;
int(*)(int *)和int(*)(const int *)不相似;
const int(*)(int *)和int(*)(int *)不相似;
int(*)(int * const)和int(*)(int *)是相似的(它们是相同的类型);
std::pair和std::pair不相似。
这个规则使得基于类型的别名分析成为可能,编译器假设通过一个类型的glvalue读取的值不会被写入到不同类型的glvalue中(受上述异常的限制)。
请注意,许多C++编译器放宽了这个规则,作为非标准的语言扩展,以允许通过联合的非活动成员访问错误类型(这种访问在C中不是未定义的)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接