【问题标题】:Does C99/C11 restrict type qualifier imply anything for functions without definition?C99/C11 限制类型限定符是否意味着没有定义的函数?
【发布时间】:2018-10-27 22:58:51
【问题描述】:

假设我们有一个函数声明,我们无法访问它的定义:

void f(int * restrict p, int * restrict q, int * restrict r);

由于我们不知道如何访问指针,我们无法知道调用是否会触发未定义的行为——即使我们传递相同的指针,如 6.7.3.1.10 中的示例解释:

函数参数声明:

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];
}

说明如何通过两个受限指针对未修改的对象进行别名。特别是,如果ab 是不相交的数组,则h(100, a, b, b) 形式的调用具有定义的行为,因为数组b 未在函数h 中修改。

因此,restrict 在这些情况下是否是多余的,除非我们对函数有更多了解,除非作为调用者的提示/注释?


例如,让我们从标准库中获取sprintf (7.21.6.6):

概要

#include <stdio.h>
int sprintf(char * restrict s,
     const char * restrict format, ...);

说明

sprintf 函数等效于fprintf,除了输出写入数组(由参数s 指定)而不是流。 (...)

从概要和描述的第一句话,我们知道s 将被写入,s 是一个受限指针。因此,我们是否可以假设(无需进一步阅读)这样的调用:

char s[4];
sprintf(s, "%s", s);

会触发未定义的行为吗?

  • 如果是,那么:sprintf的描述的最后一句话是多余的(即使澄清)?

    如果复制发生在重叠的对象之间,则行为未定义。

  • 如果不是,那么反过来:restrict 限定符是否是多余的,因为描述实际上是让我们知道什么是未定义的行为?

【问题讨论】:

    标签: c language-lawyer c99 c11 restrict


    【解决方案1】:

    如果是,那么:sprintf 描述的最后一句话是多余的吗(即使澄清)?
    如果复制发生在重叠的对象之间,则行为未定义。

    int sprintf(char * restrict s,  const char * restrict format, ...);
    

    s 上的restrict 表示读写仅取决于sprintf() 所做的事情。以下代码执行此操作,读取和写入p1 指向的数据作为char * restrict s 参数。读/写只是由于直接的sprintf() 代码而不是副作用而发生的。

    char p[100] = "abc";
    char *p1 = p;
    char *p2 = p;
    sprintf(p1, "<%s>", p2);
    

    然而当sprintf() 访问p2 指向的数据时,没有restrict。 “如果复制发生在重叠的对象之间,则行为未定义”适用于 p2,表示 p2 的数据不得因某些副作用而更改。


    如果不是,那么反过来:restrict 限定符是多余的吗,因为描述实际上是让我们知道什么是未定义的行为?

    restrict 这里是编译器实现restrict 访问。鉴于“如果发生复制...”规范,我们不需要查看它。


    考虑更简单的strcpy(),它具有相同的“如果复制发生在重叠的对象之间,则行为未定义。”。 这里的读者对我们来说是多余的,因为仔细理解restrict(C99 中的新功能)不需要它。

    char *strcpy(char * restrict s1, const char * restrict s2);
    

    C89(restrict 之前的天数)也有 strcpy(), sprintf(), ... 的这种措辞,因此可能只是 C99 中 strcpy() 的剩余规格。


    我发现type * restrict p 最具挑战性的方面是它指的是它的数据不会发生什么(p 数据不会意外更改 - 仅通过p)。然而,写入 p 的数据却被允许搞砸其他人——除非他们有 restrict

    【讨论】:

    • 人们可以编写一个行为类似于memcpy 的函数,并且具有相同的restrict 限定符,但仍然可以在不调用未定义行为的情况下处理重叠情况。诀窍是使用循环来检查范围内的所有索引是否为src+i==destdest+i==src(定义的行为,无论指针是否标识同一对象的部分)。如果有任何一个等式成立的i,则memmove(dest, (char*)dest+((char*)dest-(char*)src), n) 将定义行为。由于访问将使用从dest 派生的地址,因此不会有...
    • ...违反restrict 的要求。在某些情况下,如果知道函数的定义在参数上同时包含constrestrict,可能会允许调用端优化,而restrict 在原型对定义没有约束力。
    【解决方案2】:
    • restrict 是在 C99 中引入的。
    • Since we do not know how the pointers will be accessed, we cannot know if a call will trigger undefined behavior
      是的。但这是一个信任问题。函数声明是编写函数定义的程序员和使用函数的程序员之间的契约。请记住,一旦在 C 语言中,我们只需编写 void f(); - 这里 f 是一个接受未指定数量参数的函数。如果您不信任编写该函数的程序员,那么没有人会也不会使用该函数。在 C 中,我们传递第一个数组元素的地址,所以看到这样声明的函数,我会假设:编写该函数的程序员对这些指针的使用方式或函数 f 使用它们作为指向单个元素的指针进行了一些描述,不是数组。
      (在这种情况下,我喜欢在函数声明中使用 C99 VLA 来指定我的函数期望的数组长度:void f(int p[restrict 5], int q[restrict 10], int r[restrict 15]);。这样的函数声明与你的完全一样,但你知道什么内存不能重叠。)
    • char s[4]; sprintf(s, "%s", s); 会触发未定义的行为吗?
      是的。复制发生在重叠的对象之间,并且限制位置由两个指针访问。

    【讨论】:

    • 关于:“复制会发生在重叠的对象之间。”:“会触发UB?”的问题是关于你能不能知道没有阅读描述的最后一句话(“如果复制发生在重叠的对象之间,则行为未定义”)。
    • 关于:“...第一个和第二个参数不会重叠。它们可能与其他参数重叠。”:它们不能与任何其他指针重叠;不仅仅是其他受限指针。示例见 6.7.3.1.7。
    • 至于 C99:确实,我应该添加标签 -- 完成,谢谢!
    • 首先,我看不出int p[restrict 5] 与“C99 VLA”有什么关系。它确实是 C99 语法,但在您的示例中,它绝不涉及 VLA。其次,如果你真的期望这个大小,添加static 可能是有意义的int p[restrict static 5],因为在这种形式下,编译器甚至可能使用5 进行优化(而不是简单地忽略它)。
    【解决方案3】:

    给定一个函数签名,例如:

    void copySomeInts(int * restrict dest, int * restrict src, int n);
    

    如果有人希望函数在源和目标重叠 [或什至它们相等] 的情况下产生定义的行为,则需要付出一些额外的努力才能做到这一点。这是可能的,例如

    void copySomeInts(int * restrict dest, int const * restrict src, int n)
    {
      for (int i=0; i<n; i++)
      {
        if (dest+i == src)
        {
          int delta = src-dest;
          for (i=0; i<n; i++)
            dest[i] = dest[delta+i];
          return;
        }
        if (src+i == dest)
        {
          int delta = src-dest;
          for (i=n-1; i>=0; i--)
            dest[i] = src[delta+i];
          return;
        }
      }
      /* No overlap--safe to copy in normal fashion */
      for (int i=0; i<n; i++)
        dest[i] = src[i];
    }
    

    因此,生成代码以调用 copySomeInts 的编译器无法对其行为做出任何推断,除非它实际上可以看到定义 copySomeInts 而不仅仅是签名。

    虽然restrict 限定符并不意味着该函数无法处理源和目标重叠的情况,但它们表明这种处理可能比没有限定符时所必需的更复杂。这反过来表明,除非有明确的文档承诺处理这种情况,否则不应期望该函数以定义的方式处理它。

    请注意,在源和目标重叠的情况下,实际上并没有使用src 指针来寻址存储,也没有从它派生的任何东西。如果src+i==destdest+i==src,这意味着srcdest 都标识同一数组的元素,因此src-dest 将仅表示它们索引的差异。 src 上的 const 和 restrict 限定符意味着在函数执行期间不能以任何方式修改使用从 src 派生的指针访问的任何内容,但该限制仅适用于使用从派生的指针访问的内容src。如果这些指针实际上没有访问任何内容,则限制是空的。

    【讨论】:

    • @Deduplicator:什么测试? restrict 限定符让编译器假定不会以任何其他方式访问作为 dest 目标访问的存储或从它派生的指针,src 也是如此。我知道标准中没有任何内容会将指针的值与另一个指针的值的比较视为对指针目标的任何类型的访问,除非在指针以自身为目标的狭窄情况下。
    猜你喜欢
    • 2020-08-29
    • 1970-01-01
    • 1970-01-01
    • 2018-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-28
    相关资源
    最近更新 更多