【问题标题】:Strict aliasing rule in CC中的严格别名规则
【发布时间】:2025-12-25 21:30:10
【问题描述】:

我正在尝试理解 6.5(p6) 中定义的严格别名规则:

如果一个值通过 lvalue 的类型不是字符类型,然后是 左值成为该访问的对象的有效类型 以及不修改存储值的后续访问。

6.5(p7):

对象的存储值只能由左值访问 具有以下类型之一的表达式:88)

——与对象的有效类型兼容的类型

考虑以下示例:

struct test_internal_struct_t{
    int a;
    int b;
};

struct test_struct_t{
    struct test_internal_struct_t tis;
};

int main(){
    //alocated object, no declared type
    struct test_struct_t *test_struct_ptr = malloc(sizeof(*test_struct_ptr)); 

    //object designated by the lvalue has type int
    test_struct_ptr->tis.a = 1; 

    //object designated by the lvalue has type int
    test_struct_ptr->tis.b = 2; 

    //VIOLATION OF STRICT ALIASING RULE???
    struct test_internal_struct_t tis = test_struct_ptr->tis; 
    return 0;
}

malloc(sizeof(*test_struct_ptr)) 没有声明类型,因为它已分配,如脚注 87:

87) 分配的对象没有声明类型

通过test_struct_ptr->tis.atest_struct_ptr->tis.b 访问的对象的有效类型int。但是对象test_struct_ptr->tis自分配以来就没有有效类型。

问题:struct test_internal_struct_t tis = test_struct_ptr->tis; 是否违反了严格别名? test_struct_ptr->tis指定的对象没有有效类型,但lvaluestruct test_internal_struct_t类型。

【问题讨论】:

  • 也许我遗漏了一些东西,但我在这里看不到任何别名。有一个副本,但这不是“别名”。
  • @4386427 是的,如果将test_struct_ptr->tis 分配给变量,则会发生复制。但让我感到困惑的是,没有涉及左值 test_struct_ptr->tis 的商店,但 test_struct_ptr->tis.atest_struct_ptr->tis.b 是。所以test_struct_ptr->tis指定的对象没有有效类型(已分配),但左值本身有struct test_internal_struct_t类型。很可能我对这些部分的措辞理解有误...
  • 但是您确实分配给它......然后您将整个结构复制到 另一个 内存区域(具有自动存储持续时间)。这永远与别名无关。
  • @4386427:OP 的重点是他们存储一个int 和另一个int,然后读取struct test_internal_struct_t。那么分配的内存是如何产生struct test_internal_struct_t的有效类型的呢?
  • @4386427:我以前遇到过这种区别,C 标准的一个缺点是它没有明确说明“通过左值”存储值的含义。

标签: c language-lawyer strict-aliasing


【解决方案1】:

C 2018 6.5 6 使用短语“存储......通过左值”定义了有效类型,但从未定义该短语:

如果通过具有非字符类型类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问以及后续访问的有效类型不修改存储的值。

所以留给读者去解读。考虑这段代码:

struct a { int x; };
struct b { int x; };

void copy(int N, struct a *A, struct b *B)
{
    for (int n = 0; n < N; ++n)
        A[n].x = B[n].x;
}

如果编译器知道各种对象 A[n] 不与各种对象 B[n] 重叠,那么它可以通过在一条指令中执行多个 B[n] 的加载来优化此代码(例如 AVX 或其他单-指令多数据[SIMD]指令)并在一条指令中存储多个A[n]。 (这可能需要额外的代码来处理循环片段和对齐问题。这些与我们无关。)如果有可能某些A[n]-&gt;x 可能会引用与B[n]-&gt;x 相同的对象以获得不同的n 值,那么编译器可能不会使用这种多元素加载和存储,因为它可能会改变程序的可观察行为。例如,如果内存包含十个int,值从0到9,B指向0,而A指向2:

乙 0 1 2 3 4 5 6 7 8 9

然后写的循环,给定N = 4,必须一次复制一个元素,产生:

0 1 0 1 0 1 6 7 8 9

如果编译器将此优化为四元素加载和存储,它可以加载 0 1 2 3 然后存储 0 1 2 3,产生:

0 1 0 1 2 3 6 7 8 9

但是,C 告诉我们struct astruct b 是不兼容的,即使它们的布局相同。当XY 类型不兼容时,它告诉我们X 不是Y。类型系统的一个重要目的是区分对象类型。

现在考虑表达式A[n]-&gt;x = B[n]-&gt;x。在此:

  • A[n]struct a 的左值。
  • 由于A[n]. 的左操作数,它不会转换为值。
  • A[n].x 指定并且是 A[n] 的成员 x 的左值。
  • 右操作数的值替换A[n].x中的值。

因此,对存储值的对象的直接访问仅在 int 中,即成员 A[n].x。左值A[n] 出现在表达式中,但它不是直接用于存储值的左值。 &amp;A[n]的内存有效类型是什么?

如果我们将此内存解释为仅仅是int,那么对对象访问的唯一限制是所有B[n].x 都是int,并且所有A[n].x 都是int,所以部分或全部A[n].x 可以访问与B[n].x 的部分或全部相同的内存,并且不允许编译器进行上述优化。

这不符合类型系统区分struct astruct b的目的,因此不能正确解释。要实现预期的优化,A[n].x 存储的内存必须包含struct a 对象,B[n].x 访问的内存必须包含struct b 对象。

因此,“通过左值存储...”必须包括表达式,其中左值用于派生结构的成员,但其本身不是用于访问的最终左值。

【讨论】:

  • 既然分配的对象没有声明类型,那么对象做什么?除了分配的存储持续时间之外还有其他内容吗?
  • @SomeName:普通声明为其对象声明类型:例如,int a[3]a 声明为 3 个 int 的数组。