c - C11限制的正式定义与实现是否一致?

标签 c pointers language-lawyer c11 restrict

在尝试回答最近的问题(Passing restrict qualified pointers to functions?)时,我找不到C11标准与实践的一致性。

我并没有试图标出标准或任何东西,大多数看起来不一致的事情我只是不理解正确,但是我的问题最好是反对标准中使用的定义的论点,这里就是。

似乎可以普遍接受的是,一个函数可以使用限制限定的指针,并且既可以在该指针上工作,又可以有自己的函数调用在该指针上工作。例如,

// set C to componentwise sum and D to componentwise difference

void dif_sum(float* restrict C, float* restrict D, size_t n)
{
     size_t i = 0;
     while(i<n) C[i] = C[i] - D[i],
                D[i] += C[i] + D[i],
                ++i;
}



// set A to componentwise sum of squares of A and B
// set B to componentwise product of A and B

void prod_squdif(float* restrict A, float* restrict B, size_t n)
{
     size_t i = 0;
     float x;
     dif_sum(A,B,n);
     while(i<n) x = ( (A[i]*=A[i]) - B[i]*B[i] )/2,
                A[i] -= x,
                B[i++] = x/2;
}


似乎通常的理解是限制指针需要在其声明块内引用独立的空间。因此,prod_sqdif是有效的,因为除其指针外,在其定义块内没有任何词法访问由A或B标识的数组。

为了表明我对标准的关注,这是标准的标准strict定义(根据委员会草案,如果最终版本与其他版本有所不同,请告诉我!):


6.7.3.1限制的形式定义

1令D是一个普通标识符的声明,该声明提供了一种手段,可以将对象P指定为类型T的受限限定指针。

2如果D出现在块内并且没有存储类extern,则让B表示块。如果D出现在函数定义的参数声明列表中,则让B表示关联的块。否则,让B表示main块(或在独立环境中程序启动时调用的任何函数的块)。

3接下来,如果(在评估E之前执行B的某个顺序点处)修改P使其指向先前指向的数组对象的副本,则认为指针表达式E基于对象P。指定的指针将更改E的值。请注意,“基于”仅针对具有指针类型的表达式定义。

4在每次执行B期间,让L为具有基于P的&L的任何左值。如果使用L来访问它指定的对象X的值,并且还对X进行了修改(通过任何方式),则以下要求适用:T不得为const限定。用于访问X值的所有其他左值也应具有基于P的地址。就本款而言,每次修改X的访问也应视为修改P。如果为P分配了基于与块B2相关联的另一个受限制的指针对象P2的指针表达式E的值,则B2的执行应在B的执行之前开始,或者B2的执行应在B的执行之前结束。分配。如果这些
不满足要求,那么行为是不确定的。

5在这里,B的执行意味着程序的执行部分,它对应于标量类型和与B关联的自动存储持续时间的对象的生存期。

6译者可以随意忽略使用限制的任何或所有混叠含义。

[不包括示例,因为它们在形式上不重要。]


从6.2.4第6项的以下摘录中可以看出,识别带有词法包含在其中的表达式的B的执行:


“ ...输入一个封闭的块或调用一个函数会暂停,但不会结束,
执行当前块...”


但是,strict的形式定义的第5部分明确定义了块B,以对应于B中声明的具有自动存储的对象的生存期(在我的示例中,B是prod_squdif的主体)。显然,这将覆盖对标准中其他地方执行的块的任何定义。以下摘自标准的摘录定义了对象的生存期。


6.2.4对象的存储期限,项目2

对象的生存期是程序执行的一部分,在此期间保证为其保留存储空间。一个对象存在,具有恒定的地址,并在其整个生命周期内保留其最后存储的值。 34)如果在其生存期之外引用对象,则行为是不确定的。当指针指向的对象(或刚过去的对象)达到其生命周期的终点时,指针的值将变得不确定。


那么dif_sum的执行显然包含在B的执行中。我认为那里没有任何问题。但是,然后在dif_sum中读取和修改A和B元素(通过C和D)的左值显然不是基于A和B(它们遵循顺序点,其中A和B可以被重新指向其内容的副本,而无需更改由左值标识的位置)。这是未定义的行为。请注意,第4项中讨论的左值或序列点不受限制;如前所述,没有理由将左值和序列点限制为词法上对应于块B的那些值,因此函数调用内的左值和序列点就像在调用函数的主体中一样起作用。

另一方面,正式定义明确允许C和D被分配A和B的值,这似乎暗示了限制的普遍接受使用。这表明通过C和D对A和B的一些有意义的访问是允许的。但是,如上所述,对于通过外部或内部函数调用修改的任何元素,至少是由内部调用读取的元素,此类访问权限均未定义。这似乎与首先允许分配的明显意图相反。

当然,意图在标准中没有正式的位置,但似乎暗示了限制的通用解释而不是实际定义的意图。

总而言之,将B的执行解释为B的自动存储生命周期中每个语句的执行,然后函数调用无法与传递给它们的限制指针的内容一起使用。

在我看来,不可避免的是应该有一些例外情况,规定不考虑函数或子块内的读写,但是在这样的子块(以及其他子块,递归地)内最多只能有一个特定的限制外部块中的指针。

无论今天还是昨天,我真的都超过了标准。我真的看不到限制的正式定义如何与似乎被理解和实施的方式保持一致。

编辑:正如已经指出的那样,违反限制合同会导致不确定的行为。我的问题不是关于违反合同时会发生什么。我的问题可以重述如下:

限制的形式定义如何与通过函数调用对数组元素的访问相一致?调用函数中的此类访问是否不构成不基于传递给该函数的限制指针的访问?

我正在寻找基于标准的答案,因为我同意限制指针应该能够通过函数调用传递。似乎这不是标准中正式定义的结果。

编辑

我认为传达我的问题的主要问题与“基于”的定义有关。我将在这里尝试提出一些不同的问题。

以下是对prod_squdif的特定调用的非正式跟踪。这不打算用作C代码,而只是对函数块执行的非正式描述。

请注意,根据strict形式定义的第5项,此执行包括被调用函数的执行:“在这里执行B表示程序执行的那一部分与标量类型对象的生存期相对应,并且与B相关的自动存储期限。”

// 1. prod_squdif is called
prod_squdif( (float[1]){2}, (float[1]){1}, 1 )

// 2. dif_sum is called
dif_sum(A,B,n)   // assigns C=A and D=B

// 3. while condition is evaluated
0<1   // true

// 4. 1st assignment expression
C[0] = C[0] - D[0]   // C[0] == 0

// 5. 2nd assignment expression
D[0] += C[0] + D[0]   // D[0] == 1

// 6. increment
++i // i == 1

// 7. test
1<1   // false

// return to calling function

// 8. test
0<1   // true

// 9. 1st assignment expression
x = ( (A[0]*=A[0]) - B[1]*B[1] )/2   // x == -.5

// 10. 2nd assignment expression
A[0] -= -.5   // A[0] == .5

// 11. 3rd assignment expression
B[i++/*evaluates to 0*/] = -.5/2   // B[0] == -.25

// 12. test
1<1   // false

// prod_squdif returns


因此,对限制合同的测试由限制的形式定义中的第4条给出:“在每次执行B时,让L为具有基于P的&L的任何左值。如果使用L来访问对象的值它指定的X,并且也对X进行了修改(通过任何方式),那么以下要求适用:...用于访问X值的每个其他左值也应基于P来具有其地址...”

令L为执行(C [0])上方标为“ 4”的部分左侧的左值。 &L是否基于A?即C是否基于A?

请参见strict的形式定义的第3项:“ ...如果(在评估E之前执行B的某个顺序点上)将P修改为指向a,则指针表达式E被称为基于对象P先前指向的数组对象的副本将更改E ...的值。”

将以上第3项的结尾作为顺序点。 (在此序列点)修改A以指向它先前指向的数组对象的副本不会更改C的值。

因此,C不是基于A的。因此,A [0]是由不基于A的左值修改的。由于它也被基于A的左值读取(项10),因此这是未定义的行为。

我的问题是:因此得出结论我的示例调用了未定义的行为,因此strict的正式定义与通用实现不一致是正确的吗?

最佳答案

假设我们有一个带有嵌套块的函数,如下所示:

void foo()
{
  T *restrict A = getTptr();
  {
    T *restrict B = A;
    {
      #if hypothetical
      A = copyT(A);
      #endif
      useTptr(B + 1);
    }
  }
}


似乎在调用useTptr(B + 1)时,对A的假设更改将不再影响B + 1的值。但是,可以找到不同的序列点,因此对A的更改确实会影响B + 1的值:

void foo()
{
  T *restrict A = getTptr();
  #if hypothetical
  A = copyT(A);
  #endif    
  {
    T *restrict B = A;
    {
      useTptr(B + 1);
    }
  }
}


和C11草案标准n1570 6.7.3.1限制的形式定义仅要求存在一些这样的序列点,而不是所有序列点都表现出这种行为。

关于c - C11限制的正式定义与实现是否一致?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36346135/

相关文章:

VB.NET 中的 C 函数

c++ - 关于如何在具有单个参数的变体构造中选择替代方案?

C 编程 - 指针地址字节分割

c - 通用函数宏和如何抑制特定的 GCC 警告 : "Pointer Type Mismatch in Conditional Expression"

c++ - 在没有警告/错误的情况下对引用进行隐式重新解释

c++ - 没有默认构造函数的类对象的值初始化

c++ - 计算数字的总和乘以字符串的长度

c - 如何将指针分配给此数组(OpenCL)中某些结构的数组元素?

c - valgrind 检测到内存泄漏但应用程序工作

c++ - C++ 中的 LPWSTR、wchar_t* 和无符号短指针