当问到C语言中常见的未定义行为时,人们有时会提到严格的混叠规则。 他们在谈论什么?
当前回答
根据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;
}
because it uses an lvalue of type int to access an object of type struct S, and int is not among the types that may be used accessing a struct S. Because it would be absurd to treat all use of non-character-type members of structs and unions as Undefined Behavior, almost everyone recognizes that there are at least some circumstances where an lvalue of one type may be used to access an object of another type. Unfortunately, the C Standards Committee has failed to define what those circumstances are.
大部分问题是缺陷报告#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中也没有冲突,因为p明显地派生于结构体S,并且在下次使用S时,所有通过p访问的存储都已经发生了。
如果代码稍微改变一下……
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;
}
这里,p和对s.x的访问在被标记的行上存在别名冲突,因为在执行时存在另一个引用,该引用将用于访问相同的存储。
如果缺陷报告028说原始示例调用UB是因为两个指针的创建和使用之间有重叠,那么事情就会变得更清楚,而不必添加“有效类型”或其他类似的复杂性。
其他回答
根据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;
}
because it uses an lvalue of type int to access an object of type struct S, and int is not among the types that may be used accessing a struct S. Because it would be absurd to treat all use of non-character-type members of structs and unions as Undefined Behavior, almost everyone recognizes that there are at least some circumstances where an lvalue of one type may be used to access an object of another type. Unfortunately, the C Standards Committee has failed to define what those circumstances are.
大部分问题是缺陷报告#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中也没有冲突,因为p明显地派生于结构体S,并且在下次使用S时,所有通过p访问的存储都已经发生了。
如果代码稍微改变一下……
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;
}
这里,p和对s.x的访问在被标记的行上存在别名冲突,因为在执行时存在另一个引用,该引用将用于访问相同的存储。
如果缺陷报告028说原始示例调用UB是因为两个指针的创建和使用之间有重叠,那么事情就会变得更清楚,而不必添加“有效类型”或其他类似的复杂性。
这是严格的混叠规则,可以在c++ 03标准的3.10节中找到(其他答案提供了很好的解释,但没有一个提供了规则本身):
If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined: the dynamic type of the object, a cv-qualified version of the dynamic type of the object, a type that is the signed or unsigned type corresponding to the dynamic type of the object, a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object, an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), a type that is a (possibly cv-qualified) base class type of the dynamic type of the object, a char or unsigned char type.
c++ 11和c++ 14的措辞(强调更改):
If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined: the dynamic type of the object, a cv-qualified version of the dynamic type of the object, a type similar (as defined in 4.4) to the dynamic type of the object, a type that is the signed or unsigned type corresponding to the dynamic type of the object, a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object, an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union), a type that is a (possibly cv-qualified) base class type of the dynamic type of the object, a char or unsigned char type.
有两个变化很小:glvalue代替了lvalue,并澄清了聚合/并集的情况。
第三个变化提供了更强的保证(放宽强混叠规则):类似类型的新概念现在可以安全地进行混叠。
还有C的措辞(C99;Iso / iec 9899:1999 6.5/7;在ISO/IEC 9899:2011§6.5¶7中使用了完全相同的措辞:
An object shall have its stored value accessed only by an lvalue expression that has one of the following types 73) or 88): a type compatible with the effective type of the object, a qualified version of a type compatible with the effective type of the object, a type that is the signed or unsigned type corresponding to the effective type of the object, a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object, an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or a character type. 73) or 88) The intent of this list is to specify those circumstances in which an object may or may not be aliased.
在阅读了许多答案后,我觉得有必要补充一些东西:
严格的混叠(我将在后面描述)很重要,因为:
内存访问可能是昂贵的(就性能而言),这就是为什么数据在写回物理内存之前要在CPU寄存器中进行操作。 如果两个不同CPU寄存器中的数据将被写入相同的内存空间,那么当我们使用C语言编码时,我们无法预测哪些数据将“存活”下来。 在汇编中,我们手动编写CPU寄存器的加载和卸载代码,我们将知道哪些数据保持完整。但是C(谢天谢地)抽象了这个细节。
由于两个指针可以指向内存中的相同位置,这可能导致处理可能冲突的复杂代码。
这些额外的代码速度很慢,并且会损害性能,因为它执行额外的内存读/写操作,这些操作既慢又(可能)不必要。
严格混叠规则允许我们在假定两个指针不指向同一个内存块是安全的情况下避免冗余机器码(另请参阅restrict关键字)。
严格别名表示,可以安全地假设指向不同类型的指针指向内存中的不同位置。
如果编译器注意到两个指针指向不同的类型(例如,int *和float *),它会假设内存地址是不同的,并且不会防止内存地址冲突,从而导致更快的机器代码。
例如:
让我们假设下面的函数:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
为了处理a == b(两个指针都指向同一个内存)的情况,我们需要排序并测试从内存加载数据到CPU寄存器的方式,因此代码可能会像这样结束:
从内存中加载a和b。 把a加到b。 保存b并重新加载a。 (从CPU寄存器保存到内存,从内存加载到CPU寄存器)。 把b和a相加。 (从CPU寄存器)保存到内存。
步骤3非常慢,因为它需要访问物理内存。但是,为了防止a和b指向相同内存地址的情况,这是必需的。
严格的混叠将允许我们通过告诉编译器这些内存地址明显不同来防止这种情况(在这种情况下,这将允许进一步的优化,如果指针共享内存地址则无法执行)。
这可以通过两种方式告诉编译器,通过使用不同的类型来指向。例如: Void merge_two_numbers(int *a, long *b){…} 使用restrict关键字。例如: Void merge_two_ints(int * restrict a, int * restrict b){…}
现在,通过满足严格别名规则,可以避免步骤3,代码将运行得更快。
实际上,通过添加restrict关键字,整个函数可以优化为:
从内存中加载a和b。 把a加到b。 将结果保存到a和b。
这种优化在以前是不可能完成的,因为可能会发生冲突(a和b将是三倍而不是两倍)。
作为Doug T.已经写过的附录,在这里 是一个简单的测试用例,可能会触发GCC:
check.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汇编代码,由gcc 4.6.3生成,运行在ubuntu 12.04.2的x64上:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
所以if条件从汇编代码中完全消失了。
严格的混叠不只是指指针,它也影响引用,我为boost开发者wiki写了一篇关于它的论文,它很受欢迎,我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人们如此困惑,以及如何应对它。严格的混叠白皮书。它特别解释了为什么联合对于c++来说是危险的行为,以及为什么使用memcpy是唯一可以在C和c++之间移植的修复程序。希望这对你有帮助。
推荐文章
- 如何构建和使用谷歌TensorFlow c++ api
- C“int”的大小是2字节还是4字节?
- 多维数组如何在内存中格式化?
- printf()和puts()在C语言中的区别是什么?
- 断言是邪恶的吗?
- 下面这些短语在c++中是什么意思:0 -,default-和value-initialization?
- 在STL地图中,使用map::insert比[]更好吗?
- C++ Linux的想法?
- 有效,但毫无价值的语法在开关情况下?
- 如何为Fedora安装g++ ?
- Std::cin输入空格?
- c++标准是否要求iostreams的性能很差,或者我只是在处理一个糟糕的实现?
- 有一个好的Valgrind Windows的替代品吗?
- gcc在哪里查找C和c++头文件?
- 为什么我们需要require require ?