我仔细阅读了所有答案,但我无法得到以下示例是否为未定义行为的答案。 这与C99规范的6.7.3.1中的示例相同。
示例3函数参数声明
void h(int n, int * restrict p, int * restrict q, int * restrict r)
{
int i;
for (i = 0; i < n; i++)
p[i] = q[i] + r[i];
}
说明了如何通过两个受限制的指针对未修改的对象进行别名化。特别是,如果a和b是不相交的数组,则调用形式为h(100,a,b,b)的行为已定义,因为数组b未在函数h中修改。
简而言之,如果b未在函数h中修改,则明确提到它的定义行为。但是,如果调用h(100, a, a, b)
?
更多背景为什么我想说清楚。我们希望以就地或不合适的方式使用一些基本功能。为了减少工作量,我们希望我们不需要同时提供h(int n, int * restrict p, int * restrict q, int * restrict r)
和h_inplace(int n, int * restrict p, int * restrict q)
。从目前的观察来看,即使我们将其称为h(100, a, a, b)
的形式,gcc,clang,icc,msvc似乎也能给出正确的结果。但是,如果它是未定义的行为,我们绝对不希望有风险(这意味着它可能是其他编译器或gcc,clang,icc,msvc的未来版本的错误)。你觉得怎么样?
答案 0 :(得分:4)
h(100, a, a, b)
显然会导致UB,因为p
和q
承诺不会互为别名,并且代码通过其中一个写入。见C11 6.7.3.1/4:
如果
的地址L
用于访问其指定的对象X
的值,则和X
也会被修改(无论如何),然后以下要求适用:[...]用于访问X
值的每个其他左值也应基于P
通过p
写入访问对象的值(此处称为X
),并对其进行修改。因此,必须从p
生成用于访问函数内对象的每个左值。但是q
未生成p
。
答案 1 :(得分:1)
这是代码优化器的提示。使用restrict可确保它可以将指针变量存储在CPU寄存器中,而不必将指针值的更新刷新到内存中,以便更新别名。
它是否利用它在很大程度上取决于优化器和CPU的实现细节。代码优化器已经在检测非混叠方面投入了大量资金,因为它是如此重要的优化。
但是,如果使用restrict关键字并且该函数声明为
void updatePtrs(size_t *restrict ptrA, size_t *restrict ptrB, size_t *restrict val);
然后允许编译器假设ptrA,ptrB和val指向不同的位置,更新一个指针不会影响其他指针。程序员而不是编译器负责确保指针不指向相同的位置。
如果作为程序员你没有确保ptrA,ptrB指向不同的位置,那么很明显你违反了可能导致未定义行为的规则。
答案 2 :(得分:0)
很明显,正如M.M的第一个回答所述,h(100,a,a,b)是未定义的行为,但是有可能创建一个低开销版本的h,确实有定义的行为(国际海事组织:我目前与理查德有关于此的争论。)
void h2(int n, int * restrict p, int * restrict q_opt, int * restrict r)
{
int i;
int * restrict q = (p == q_opt) ? p : q_opt;
for (i = 0; i < n; i++)
p[i] = q[i] + r[i];
}
更简单的版本(但要求调用者知道魔术值)有新的行:
int * restrict q = q_opt ? q_opt : p;
并且调用者通过调用h2(100,a,0,b)来获取就地行为。
在这两种情况下,q指针根据限制规则与p不同,或者从p(而不是q)导出,因此不会发生冲突。
我认为,特别是标准中的讨论和理由实际上非常好。限制的原因是,有时可以通过首先将限制指针所寻址的任何数据复制到另一个存储空间(每个参数)上,然后将结果复制回来,来制作更快的代码。当额外的内存副本进入寄存器时尤其如此,当允许编译器展开循环时,这是真的,所有这些都是很好的快速工具。
当q_opt中的源在使用之前(在使用之前,在循环之外)被复制到另一个位置时,无论是指p还是指向不同的数组,这里的策略都可以正常工作。如果用户调用h2(100,a + 1,a,r),它显然会失败,因为这仍然违反了限制规则。