代码之家  ›  专栏  ›  技术社区  ›  Benoit

什么是严格的别名规则?

  •  713
  • Benoit  · 技术社区  · 16 年前

    当问到 common undefined behavior in C 灵魂比我所说的更开明,严格的化名规则。
    他们在说什么?

    11 回复  |  直到 16 年前
        1
  •  514
  •   community wiki 30 revs, 15 users 75% Doug T.    6 年前

    遇到严格别名问题的典型情况是,将结构(如设备/网络消息)叠加到系统字大小的缓冲区(如指向 uint32_t S或 uint16_t s)。当您将一个结构覆盖到这样的缓冲区上,或者通过指针投射将缓冲区覆盖到这样的结构上时,很容易违反严格的别名规则。

    所以在这种设置中,如果我想向某个对象发送消息,就必须有两个不兼容的指针指向同一块内存。然后我可能天真地编写这样的代码:

    typedef struct Msg
    {
        unsigned int a;
        unsigned int b;
    } Msg;
    
    void SendWord(uint32_t);
    
    int main(void)
    {
        // Get a 32-bit buffer from the system
        uint32_t* buff = malloc(sizeof(Msg));
    
        // Alias that buffer through message
        Msg* msg = (Msg*)(buff);
    
        // Send a bunch of messages    
        for (int i =0; i < 10; ++i)
        {
            msg->a = i;
            msg->b = i+1;
            SendWord(buff[0]);
            SendWord(buff[1]);   
        }
    }
    

    严格的别名规则使此设置非法:取消引用对非 compatible type 或C 2011 6.5第7段允许的其他类型之一 是未定义的行为。不幸的是,您仍然可以这样编码, 也许吧 得到一些警告,让它编译得很好,只会在运行代码时出现奇怪的意外行为。

    (GCC给出别名警告的能力似乎有些不一致,有时给出友好警告,有时则没有。)

    为了了解这种行为为什么没有定义,我们必须考虑一下严格的别名规则购买编译器的原因。基本上,使用这个规则,不需要考虑插入指令来刷新 buff 每次循环。相反,在优化时,使用一些关于别名的令人讨厌的非强制假设,它可以忽略这些指令,加载 buff[0] buff[1 ]在循环运行之前,进入CPU寄存器一次,并加速循环体。在引入严格的别名之前,编译器必须处于一种偏执的状态,即 浅黄色 任何人都可以随时随地改变。为了获得额外的性能优势,并且假设大多数人不输入双关指针,引入了严格的混叠规则。

    请记住,如果您认为该示例是人为的,那么即使您正在将缓冲区传递给另一个为您执行发送的函数,也可能会发生这种情况,如果您已经这样做了的话。

    void SendMessage(uint32_t* buff, size_t size32)
    {
        for (int i = 0; i < size32; ++i) 
        {
            SendWord(buff[i]);
        }
    }
    

    并重写我们之前的循环以利用这个方便的功能

    for (int i = 0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendMessage(buff, 2);
    }
    

    编译器可能或可能无法或不够聪明地尝试内联sendmessage,并且可能或可能不会决定再次加载buff。如果 SendMessage 是另一个单独编译的API的一部分,它可能有加载buff内容的指令。然后,也许你在C++中,这是编译器认为它可以内嵌的一些模板头的实现。或者只是为了方便起见,你在.c文件中写了一些东西。不管怎样,未定义的行为仍然可能发生。即使我们知道一些发生在引擎盖下的事情,它仍然是违反规则的,所以没有明确的行为是可以保证的。因此,仅仅通过包装一个接受单词分隔缓冲区的函数并不一定有帮助。

    那我该怎么解决这个问题呢?

    • 使用工会。大多数编译器支持这一点,而不抱怨严格的别名。这在C99中是允许的,在C11中是明确允许的。

      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
      
    • 可以在编译器中禁用严格的别名( f[no-]strict-aliasing 在海湾合作委员会)

    • 你可以使用 char* 用于别名而不是系统的字。规则允许例外 烧焦* (包括) signed char unsigned char )人们总是认为 烧焦* 别名其他类型。但是,这不会以另一种方式工作:不存在这样的假设:您的结构别名是一个字符缓冲区。

    初学者谨防

    当两种类型叠加在一起时,这只是一个潜在的雷区。你也应该了解 endianness , word alignment 以及如何通过 packing structs 正确地。

    脚注

    C 2011 6.5 7允许左值访问的类型有:

    • 与对象的有效类型兼容的类型,
    • 与对象的有效类型兼容的类型的限定版本,
    • 与对象的有效类型相对应的有符号或无符号类型的类型,
    • 与对象有效类型的限定版本相对应的有符号或无符号类型的类型,
    • 聚合或联合类型,其成员中包含上述类型之一(递归地包括子聚合或包含的联合的成员),或
    • 字符类型。
        2
  •  222
  •   Palec    7 年前

    我发现最好的解释是迈克·阿克顿, Understanding Strict Aliasing . 它将重点放在PS3开发上,但这基本上只是GCC。

    从文章中:

    “严格混叠”是C(或C++)编译器提出的一个假设,即将指针指向不同类型的对象将永远不会引用相同的内存位置(即彼此别名)。

    所以基本上如果你有 int* 指向包含 int 然后你点A float* 把它当作一个 float 你违反了规则。如果代码不尊重这一点,那么编译器的优化器很可能会破坏代码。

    规则的例外是 char* ,允许指向任何类型。

        3
  •  127
  •   M.M    7 年前

    这是严格的别名规则,可在 C++ 03 标准(其他答案提供了很好的解释,但没有提供规则本身):

    如果程序试图通过下列类型之一以外的左值访问对象的存储值,则行为未定义:

    • 对象的动态类型,
    • 对象动态类型的cv限定版本,
    • 与对象的动态类型相对应的有符号或无符号类型的类型,
    • 与对象的动态类型的cv限定版本相对应的有符号或无符号类型的类型,
    • 聚合或联合类型,在其成员中包括上述类型之一(递归地包括子聚合或包含的联合的成员)。
    • 是对象动态类型的(可能是cv限定的)基类类型的类型,
    • char unsigned char 类型。

    C++ 11 C++ 14 措辞(强调变更):

    如果程序试图通过 GLUVE 除以下类型之一外,行为未定义:

    • 对象的动态类型,
    • 对象动态类型的cv限定版本,
    • 与对象的动态类型类似的类型(如4.4中所定义),
    • 与对象的动态类型相对应的有符号或无符号类型的类型,
    • 与对象的动态类型的cv限定版本相对应的有符号或无符号类型的类型,
    • 一种聚合或联合类型,其中包括上述类型之一 元素或非静态数据成员 (递归地包括 元素或非静态数据成员 亚集合体或包含的联合体)
    • 是对象动态类型的(可能是cv限定的)基类类型的类型,
    • 烧焦 无符号字符 类型。

    两个变化很小: GLUVE 而不是 左值 以及对合并案/工会案的澄清。

    第三个变化提供了更强的保证(放宽强混叠规则):新概念 相似类型 现在可以安全地使用别名了。


    C 措辞(C99;ISO/IEC 9899:1999 6.5/7;ISO/IEC 9899:2011_§6.5_¶7中使用的措辞完全相同):

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

    • 与对象的有效类型兼容的类型,
    • 与有效类型兼容的类型的限定版本 对象,
    • 一种类型,它是与 对象的有效类型,
    • 一种类型,它是与 对象有效类型的限定版本,
    • 包含上述其中一种的聚合或联合类型 其成员中的类型(递归地包括 亚集合体或包含的联合体),或
    • 字符类型。

    73)或88) 此列表的目的是指定对象可能有别名,也可能没有别名的情况。

        4
  •  41
  •   phorgan1    14 年前

    严格的别名不仅仅指指针,它也影响引用,我为BoostDeveloper wiki写了一篇关于它的文章,得到了很好的认可,我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人们如此困惑,以及如何处理它。 Strict Aliasing White Paper . 特别地,它解释了为什么工会是C++的冒险行为,以及为什么使用MeMCPY是C和C++之间唯一的可移植的解决方案。希望这是有帮助的。

        5
  •  37
  •   Micha Wiedenmann Lieven Keersmaekers    6 年前

    注释

    这是从我的 "What is the Strict Aliasing Rule and Why do we care?" 写起来。

    什么是严格的别名?

    在C和C++中,混叠与我们允许通过哪些表达式类型访问存储的值有关。在C和C++中,标准指定允许哪些类型的别名属于哪种类型。编译器和优化器可以假定我们严格遵循别名规则,因此术语 严格别名规则 . 如果我们试图使用不允许的类型访问一个值,它被分类为 undefined behavior ( UB )一旦我们有了未定义的行为,所有的赌注都取消了,我们程序的结果就不再可靠了。

    不幸的是,由于存在严格的别名冲突,我们通常会得到我们期望的结果,这就使得未来版本的编译器有了新的优化,可能会破坏我们认为有效的代码。这是不可取的,理解严格的别名规则以及如何避免违反它们是值得的目标。

    为了了解更多我们关心的原因,我们将讨论违反严格混叠规则时出现的问题,因为在类型混叠中使用的常见技术经常违反严格混叠规则,以及如何正确键入混叠。

    初步示例

    让我们看一些例子,然后我们可以确切地讨论标准所说的内容,检查一些进一步的例子,然后看看如何避免严格的别名和捕获我们遗漏的冲突。这是一个不应该令人惊讶的例子( live example ):

    int x = 10;
    int *ip = &x;
    
    std::cout << *ip << "\n";
    *ip = 12;
    std::cout << x << "\n";
    

    我们有一个 INT* 指向被 int 这是一个有效的别名。优化器必须假定通过 知识产权 无法更新占用的值 X .

    下一个示例显示导致未定义行为的别名( live example ):

    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?
    }
    

    在函数中 我们采取了 INT* 和A 浮动* ,在这个例子中,我们称之为 并将这两个参数设置为指向相同的内存位置,在本例中,该位置包含 int . 注意, reinterpret_cast 指示编译器将表达式视为其模板参数指定的类型。在这种情况下,我们告诉它处理表达式 &X 好像它有类型 浮动* . 我们可能天真地期望第二个结果 咳嗽 成为 但如果启用了优化,则使用 -O2 GCC和Clang均产生以下结果:

    0
    1
    

    这可能不是预期的,但完全有效,因为我们调用了未定义的行为。一 浮动 不能有效地别名an int 对象。因此,优化器可以假定 常数1 取消引用时存储 将是从存储到 f 不能有效地影响 int 对象。在编译器资源管理器中插入代码表明这正是发生的事情( live example ):

    foo(float*, int*): # @foo(float*, int*)
    mov dword ptr [rsi], 1  
    mov dword ptr [rdi], 0
    mov eax, 1                       
    ret
    

    优化器使用 Type-Based Alias Analysis (TBAA) 假设 将返回并直接将常量值移入寄存器 埃克斯 带有返回值。tbaa使用有关允许别名的类型的语言规则来优化加载和存储。在这种情况下,TBAA知道 浮动 不能别名和 int 并优化了 .

    现在,到规则手册

    标准究竟规定了我们被允许和不被允许做什么?标准语言并不简单,因此对于每一项,我都将尝试提供代码示例来演示其含义。

    C11标准怎么说?

    这个 C11 标准在第节中说明了以下内容 6.5表达第7段 :

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

    int x = 1;
    int *p = &x;   
    printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
    

    与对象有效类型兼容的类型的合格版本,

    int x = 1;
    const int *p = &x;
    printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
    

    _是与对象的有效类型相对应的有符号或无符号类型,

    int x = 1;
    unsigned int *p = (unsigned int*)&x;
    printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                         // the effective type of the object
    

    gcc/clang has an extension also 允许分配 无符号INT* INT* 即使它们是不兼容的类型。

    _是与对象有效类型的限定版本相对应的有符号或无符号类型,

    int x = 1;
    const unsigned int *p = (const unsigned int*)&x;
    printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                         // that corresponds with to a qualified verison of the effective type of the object
    

    一种聚合或联合类型,其成员中包括上述类型之一(递归地包括子聚合或包含联合的成员),或

    struct foo {
      int x;
    };
    
    void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                             // can alias with *ip
    
    foo f;
    foobar( &f, &f.x );
    

    字符类型。

    int x = 65;
    char *p = (char *)&x;
    printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                          // The results are not portable due to endianness issues.
    

    C++ 17草案标准说什么

    C++ 17版标准草案 [基本等级]第11段 说:

    如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义: 六十三 (11.1)对象的动态类型,

    void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
    int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
    std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                      // of the allocated object
    

    (11.2)对象动态类型的CV合格版本,

    int x = 1;
    const int *cip = &x;
    std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                                // version of the dynamic type of x
    

    (11.3)_

    (11.4)与对象的动态类型相对应的有符号或无符号类型,

    // Both si and ui are signed or unsigned types corresponding to each others dynamic types
    // We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
    signed int foo( signed int &si, unsigned int &ui ) {
      si = 1;
      ui = 2;
    
      return si;
    }
    

    (11.5)与对象动态类型的cv限定版本相对应的有符号或无符号类型,

    signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
    

    (11.6)_

    struct foo {
     int x;
    };
    
    // Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
    int foobar( foo &fp, int &ip ) {
     fp.x = 1;
     ip = 2;
    
     return fp.x;
    }
    
    foo f; 
    foobar( f, f.x ); 
    

    (11.7)_是对象动态类型的(可能是符合cv的)基本类类型,

    struct foo { int x ; };
    
    struct bar : public foo {};
    
    int foobar( foo &f, bar &b ) {
      f.x = 1;
      b.x = 2;
    
      return f.x;
    }
    

    (11.8)字符、无符号字符或std::byte类型。

    int foo( std::byte &b, uint32_t &ui ) {
      b = static_cast<std::byte>('a');
      ui = 0xFFFFFFFF;                   
    
      return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                         // an object of type uint32_t
    }
    

    值得注意的 符号字符 不包括在上面的列表中,这与 C 哪说 字符类型 .

    什么是双关拳

    我们已经到了这一点,我们可能想知道,为什么我们要化名为?答案通常是 双关语 ,通常使用的方法违反了严格的别名规则。

    有时我们想绕过类型系统,将对象解释为不同的类型。这叫做 类型双关 ,将内存段重新解释为另一种类型。 型双关 对于希望访问对象的基础表示形式以进行查看、传输或操作的任务很有用。我们发现使用的典型类型punning是编译器、序列化、网络代码等。

    传统上,这是通过获取对象的地址,将其转换为我们想要重新解释的类型的指针,然后访问该值,或者换句话说,通过别名来实现的。例如:

    int x =  1 ;
    
    // In C
    float *fp = (float*)&x ;  // Not a valid aliasing
    
    // In C++
    float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing
    
    printf( "%f\n", *fp ) ;
    

    正如我们前面看到的,这不是一个有效的别名,因此我们正在调用未定义的行为。但是传统的编译器并没有利用严格的别名规则,这种类型的代码通常只起作用,开发人员不幸地习惯了这种方式。对于类型punning,一种常见的替代方法是通过unions,它在c中有效,但是 未定义的行为 在C++中 see live example ):

    union u1
    {
      int n;
      float f;
    } ;
    
    union u1 u;
    u.f = 1.0f;
    
    printf( "%d\n”, u.n );  // UB in C++ n is not the active member
    

    这在C++中是无效的,有些人认为工会的目的仅仅是为了实现变体类型,并认为使用工会进行类型双关是一种滥用。

    我们如何正确地输入双关语?

    标准方法 类型双关 C和C++都是 曼皮西 . 这看起来有点笨手笨脚,但优化器应该认识到 曼皮西 对于 类型双关 并对其进行优化,生成寄存器进行寄存器移动。例如,如果我们知道 英特64 和一样大小 双重的 :

    static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message
    

    我们可以使用 曼皮西 :

    void func1( double d ) {
      std::int64_t n;
      std::memcpy(&n, &d, sizeof d); 
      //...
    

    在充分的优化级别上,任何合适的现代编译器都会生成与前面提到的相同的代码 重新解释铸模 方法或 联盟 方法 类型双关 . 检查生成的代码,我们看到它只使用寄存器mov( live Compiler Explorer Example )

    C++ 20与BITICAST CAST

    在C++ 20中我们可以获得 比特铸 ( implementation available in link from proposal )这为输入pun提供了一种简单而安全的方法,并且可以在constexpr上下文中使用。

    以下是如何使用的示例 比特铸 键入双关语 无符号整型 浮动 , see it live ):

    std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
    

    在这种情况下 类型没有相同的大小,它要求我们使用中间结构15。我们将使用包含 sizeof(无符号int) 字符数组( 假设4字节无符号整数 要成为 类型及 无符号整型 作为 类型:

    struct uint_chars {
     unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
    };
    
    // Assume len is a multiple of 4 
    int bar( unsigned char *p, size_t len ) {
     int result = 0;
    
     for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
       uint_chars f;
       std::memcpy( f.arr, &p[index], sizeof(unsigned int));
       unsigned int result = bit_cast<unsigned int>(f);
    
       result += foo( result );
     }
    
     return result ;
    }
    

    不幸的是,我们需要这个中间类型,但这是 比特铸 .

    捕获严格的别名冲突

    我们没有很多很好的工具来捕获C++中的严格混叠,我们所使用的工具将捕获一些严格的混叠违反和一些错误的负载和存储的情况。

    GCC使用标志 -fstrict别名 -wstrict别名 可以捕捉到一些情况,尽管不是没有误报/否定。例如,以下情况将在GCC中生成警告( see it live ):

    int a = 1;
    short j;
    float f = 1.f; // Originally not initialized but tis-kernel caught 
                   // it was being accessed w/ an indeterminate value below
    
    printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
    printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
    

    尽管它不能抓住这个额外的案子( see it live ):

    int *p;
    
    p=&a;
    printf("%i\n", j = *(reinterpret_cast<short*>(p)));
    

    尽管clang允许使用这些标志,但它显然不实现警告。

    我们可以使用的另一个工具是asan,它可以捕获未对齐的加载和存储。虽然这些不是直接严格的别名冲突,但它们是严格的别名冲突的常见结果。例如,以下情况将在使用clang生成时生成运行时错误 -fsanitize=地址

    int *x = new int[2];               // 8 bytes: [0,7].
    int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
    *u = 1;                            // Access to range [6-9]
    printf( "%d\n", *u );              // Access to range [6-9]
    

    我推荐的最后一个工具是C++专用的,不是严格的工具,而是编码实践,不允许C样式的转换。GCC和Clang都将使用 -沃尔德式铸造 . 这将强制任何未定义的类型pun使用reinterpret-cast,一般来说,reinterpret-cast应该是用于更近的代码检查的标志。在代码库中搜索重新解释代码以执行审计也更容易。

    对于C语言,我们已经介绍了所有的工具,我们也有TIS解释器,它是一个静态分析器,可以详尽地分析C语言的一大个子集的程序。给出了前面示例的验证,其中使用 -fstrict别名 错过一个案例( see it live )

    int a = 1;
    short j;
    float f = 1.0 ;
    
    printf("%i\n", j = *((short*)&a));
    printf("%i\n", j = *((int*)&f));
    
    int *p; 
    
    p=&a;
    printf("%i\n", j = *((short*)p));
    

    tis interpeter能够捕获这三个,下面的示例将tis kernal作为tis解释器调用(为简洁起见,编辑输出):

    ./bin/tis-kernel -sa example1.c 
    ...
    example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
                  rules by accessing a cell with effective type int.
    ...
    
    example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
                  accessing a cell with effective type float.
                  Callstack: main
    ...
    
    example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
                  accessing a cell with effective type int.
    

    终于有了 TySan 目前正在开发中。这个消毒剂在阴影内存段中添加类型检查信息,并检查访问是否违反了别名规则。该工具可能能够捕获所有别名冲突,但可能会有很大的运行时开销。

        6
  •  32
  •   Ingo Blackman    12 年前

    作为Doug T.已经写的附录,这里 是一个简单的测试用例,它可能会触发GCC:

    检查C

    #include <stdio.h>
    
    void check(short *h,long *k)
    {
        *h=5;
        *k=6;
        if (*h == 5)
            printf("strict aliasing problem\n");
    }
    
    int main(void)
    {
        long      k[1];
        check((short *)k,k);
        return 0;
    }
    

    编译用 gcc -O2 -o check check.c . 通常(在我尝试的大多数gcc版本中),这会输出“严格的别名问题”,因为编译器假定“h”不能与“check”函数中的“k”地址相同。因此,编译器优化 if (*h == 5) 离开并始终调用printf。

    对于那些在这里感兴趣的人来说,X64汇编程序代码,由GCC4.6.3生成,运行在Ubuntu 12.04.2上,用于X64:

    movw    $5, (%rdi)
    movq    $6, (%rsi)
    movl    $.LC0, %edi
    jmp puts
    

    所以if条件完全从汇编程序代码中消失了。

        7
  •  15
  •   C. K. Young    11 年前

    Type punning via指针强制转换(而不是使用联合)是打破严格混叠的主要示例。

        8
  •  12
  •   supercat    7 年前

    根据C89的基本原理,该标准的作者不希望要求编译器提供如下代码:

    int x;
    int test(double *p)
    {
      x=5;
      *p = 1.0;
      return x;
    }
    

    需要重新加载 x 在赋值语句和返回语句之间,以便允许 p 可能指向 X 以及分配给 *p 因此可能改变 X . 编译器有权假定不存在别名的概念 在上述情况下 没有争议。

    不幸的是,C89的作者以一种方式编写了他们的规则,如果从字面上看,甚至会使以下函数调用未定义的行为:

    void test(void)
    {
      struct S {int x;} s;
      s.x = 1;
    }
    

    因为它使用类型的左值 int 访问类型的对象 struct S int 不在可用于访问 结构体 . 因为将结构和联合的非字符类型成员的所有使用都视为未定义的行为是荒谬的,几乎所有人都认识到,至少在某些情况下,一种类型的左值可用于访问另一种类型的对象。不幸的是,C标准委员会未能确定这些情况是什么。

    大部分问题都是缺陷报告028的结果,该报告询问了程序的行为,例如:

    int test(int *ip, double *dp)
    {
      *ip = 1;
      *dp = 1.23;
      return *ip;
    }
    int test2(void)
    {
      union U { int i; double d; } u;
      return test(&u.i, &u.d);
    }
    

    缺陷报告28指出,程序调用未定义的行为,因为写入“double”类型的联合成员并读取“int”类型的联合成员的操作调用实现定义的行为。这种推理是无意义的,但形成了有效类型规则的基础,这些规则不必要地使语言复杂化,而不做任何事情来解决原始问题。

    解决原始问题的最佳方法可能是 关于规则目的的脚注,好像它是规范性的,并且 规则不可执行,除非实际涉及使用别名的冲突访问。给出如下信息:

     void inc_int(int *p) { *p = 3; }
     int test(void)
     {
       int *p;
       struct S { int x; } s;
       s.x = 1;
       p = &s.x;
       inc_int(p);
       return s.x;
     }
    

    里面没有冲突 inc_int 因为所有对存储的访问都是通过 *P 使用类型的左值完成 int 没有冲突 test 因为 显然是从 结构体 到下一次 s 所有访问该存储的 已经发生了。

    如果代码稍微改变了…

     void inc_int(int *p) { *p = 3; }
     int test(void)
     {
       int *p;
       struct S { int x; } s;
       p = &s.x;
       s.x = 1;  //  !!*!!
       *p += 1;
       return s.x;
     }
    

    这里,在 以及 s.x 在标记的行上,因为在执行的该点上存在另一个引用 将用于访问同一个存储 .

    如果缺陷报告028说,最初的例子调用UB是因为两个指针的创建和使用之间存在重叠,那么在不添加“有效类型”或其他此类复杂性的情况下,这会使事情变得更加清楚。

        9
  •  9
  •   Myst    7 年前

    在阅读了许多答案之后,我觉得有必要增加一些内容:

    严格的别名(我将用一点描述) 很重要,因为 :

    1. 内存访问可能很昂贵(从性能上讲),这就是为什么 在CPU寄存器中操作数据 在写回物理内存之前。

    2. 如果两个不同的CPU寄存器中的数据写入同一内存空间, 我们无法预测哪些数据将“存活” 当我们用C编码时。

      在汇编中,我们手工编码CPU寄存器的加载和卸载,我们将知道哪些数据保持完整。但谢天谢地,C抽象了这个细节。

    由于两个指针可以指向内存中的同一位置,这可能导致 处理可能的冲突的复杂代码 .

    这个额外的代码很慢而且 影响性能 因为它执行额外的内存读/写操作,这既慢又(可能)不必要。

    这个 严格的别名规则允许我们避免冗余的机器代码 在这种情况下 应该是 假设两个指针不指向同一内存块(另请参见 restrict 关键字)。

    严格的别名声明可以安全地假定指向不同类型的指针指向内存中的不同位置。

    如果编译器发现两个指针指向不同的类型(例如, int * 和A float * ,它将假定内存地址不同,并且 不会 防止内存地址冲突,导致更快的机器代码。

    例如 :

    让我们假设以下函数:

    void merge_two_ints(int *a, int *b) {
      *b += *a;
      *a += *b;
    }
    

    为了处理 a == b (两个指针都指向同一个内存),我们需要对从内存向CPU寄存器加载数据的方式进行排序和测试,因此代码最终可能如下所示:

    1. 负载 a b 从记忆中。

    2. 添加 .

    3. 节约 再装填 .

      (从CPU寄存器保存到内存,并从内存加载到CPU寄存器)。

    4. 添加 .

    5. 节约 (从CPU寄存器)到内存。

    步骤3非常慢,因为它需要访问物理内存。但是,它需要防止出现以下情况: 指向相同的内存地址。

    严格的别名允许我们通过告诉编译器这些内存地址明显不同来防止这种情况(在本例中,这将允许进一步优化,如果指针共享内存地址,则无法执行进一步优化)。

    1. 这可以通过两种方式告诉编译器,即使用不同的类型来指向。即。:

      void merge_two_numbers(int *a, long *b) {...}
      
    2. 使用 限制 关键字。即。:

      void merge_two_ints(int * restrict a, int * restrict b) {...}
      

    现在,通过满足严格的别名规则,可以避免步骤3,代码运行速度将显著加快。

    实际上,通过添加 限制 关键字,整个函数可以优化为:

    1. 负载 从记忆中。

    2. 添加 .

    3. 将结果同时保存到 并且 .

    由于可能发生碰撞(在 是三倍而不是两倍)。

        10
  •  5
  •   godel9    11 年前

    严格的别名不允许对同一数据使用不同的指针类型。

    This article 应该有助于你全面了解这个问题。

        11
  •  -2
  •   curiousguy    7 年前

    在C++技术上,严格的混叠规则可能是不适用的。

    注意间接性的定义( * operator ):

    一元*运算符执行间接寻址:它所指向的表达式 应用的应为指向对象类型的指针,或指向 功能类型和 结果是引用对象的左值 或 功能 表达式指向的对象 .

    也从 the definition of glvalue

    glvalue是一个表达式,其计算确定 一个物体,(…狙击)

    所以在任何定义良好的程序跟踪中,glvalue都引用一个对象。 所以所谓的严格别名规则永远都不适用。 这可能不是设计师想要的。