C99/C11对于没有定义的函数是否限制类型限定符有何影响?

4
假设我们有一个函数声明,但我们无法访问其定义:
void f(int * restrict p, int * restrict q, int * restrict r);

由于我们不知道指针将如何被访问,因此即使我们传递相同的指针(就像6.7.3.1.10中的示例所解释的那样),我们也无法确定调用是否会触发未定义行为。

The function parameter declarations:

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

illustrate how an unmodified object can be aliased through two restricted pointers. In particular, if a and b are disjoint arrays, a call of the form h(100, a, b, b) has defined behavior, because array b is not modified within function h.

因此,在这些情况下,除非我们对函数有更多了解,否则restrict 是多余的,只是用作对调用者的提示/注释。
例如,让我们来看一下标准库中的sprintf(7.21.6.6)。

Synopsis

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

Description

The sprintf function is equivalent to fprintf, except that the output is written into an array (specified by the argument s) rather than to a stream. (...)

从简介和描述的第一句话中,我们知道s将被写入,并且s是一个受限指针。因此,我们可以假设(无需继续阅读)会出现以下调用:

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

会触发未定义行为吗?

  • 如果是,则:即使澄清,sprintf描述的最后一句话是否多余?

    如果在重叠对象之间进行复制,行为未定义。

  • 如果不是,则相反:restrict限定符是否多余,因为描述实际上让我们知道将会出现未定义的行为?

3个回答

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(),它具有相同的“如果在重叠对象之间进行复制,则行为未定义。”。这对我们读者来说是多余的,因为仔细理解C99中的restrict就不需要这个了。
char *strcpy(char * restrict s1, const char * restrict s2);

C89(在使用restrict之前)对于strcpy(), sprintf()...也有这种措辞,因此C99中strcpy()可能只是一个过度规范的遗留问题。
我认为type * restrict p最具挑战性的方面在于它指的是其数据不会发生意外改变(p数据只能通过p更改)。然而,写入p数据允许破坏其他数据-除非它们有一个restrict

1
可以编写一个与memcpy行为完全相同且具有相同restrict限定符的函数,但仍然处理重叠情况而不会引发未定义行为。关键在于使用循环检查范围内所有索引处的src+i==destdest+i==src(无论指针是否标识同一对象的部分),这是已定义的行为。如果存在任何一个i使得其中一个等式成立,则memmove(dest, (char*)dest+((char*)dest-(char*)src), n)将具有已定义的行为。由于访问将使用从dest派生的地址,因此不会... - supercat
1
违反了restrict的要求。有些情况下,知道一个函数的定义在参数上同时包含constrestrict可能会允许调用方进行优化,否则是不可能的,但原型中存在restrict并不意味着它对定义具有约束力。 - supercat

1
  • restrict是C99中引入的。
  • 由于我们不知道指针将如何被访问,因此无法知道调用是否会触发未定义的行为
    是的。但这是一个信任问题。函数声明是一个合同,由编写函数定义的程序员和使用函数的程序员之间达成的协议。请记住,在C语言中,我们只需编写void f(); - 这里f是一个带有未指定参数数量的函数。如果你不信任编写该函数的程序员,没有人会使用该函数。在C语言中,我们传递第一个数组元素的地址,因此看到这样声明的函数,我会假设:编写该函数的程序员对如何使用这些指针进行了一些说明,或者函数f将它们用作单个元素的指针,而不是数组。
    (在这种情况下,我喜欢在函数声明中使用C99 VLAs来指定函数期望的数组长度:void f(int p[restrict 5], int q[restrict 10], int r[restrict 15]);。这样的函数声明与您的完全相等,但您知道哪些内存不能重叠。)
  • char s[4]; sprintf(s, "%s", s);会触发未定义的行为吗?
    是的。复制发生在重叠的对象之间,并且限制位置由两个指针访问。

关于:“在重叠的对象之间进行拷贝”:问题“是否会触发UB?”是关于你是否可以在没有阅读描述的最后一句话(“如果在重叠的对象之间进行拷贝,行为将是未定义的”)的情况下知道这一点。 - Acorn
关于:“*...第一个和第二个参数不会重叠。它们可能与其他参数重叠。”:它们不能与任何其他指针重叠;而不仅仅是与其他受限制的指针重叠。请参见6.7.3.1.7中的示例。 - Acorn
关于C99:确实,我应该添加标签 - 已完成,谢谢! - Acorn
首先,我不明白 int p[restrict 5] 与 "C99 VLAs" 有什么关系。它确实是 C99 语法,但在你的示例中,它并没有涉及到任何 VLA。其次,如果你真的期望这个大小,添加 static 可能是有意义的 int p[restrict static 5],因为在这种形式下,编译器甚至可以将 5 用于优化(而不是简单地忽略它)。 - AnT stands with Russia

0

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

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==dest 或者 dest+i==src,那就意味着 srcdest 都标识同一数组的元素,因此 src-dest 只是表示它们索引之间的差异。src 上的 const 和 restrict 限定符意味着在函数执行期间,使用从 src 派生的指针访问的任何内容都无法进行修改,但该限制仅适用于使用从 src 派生的指针访问的内容。如果实际上没有使用这些指针来访问任何内容,则该限制是无效的。

@Deduplicator:什么测试?restrict限定符邀请编译器假设以dest为目标或从其派生的指针访问的存储不会以其他方式访问,src同理。我不知道标准中是否有任何关于将指针的值与另一个指针的值进行比较被视为对指针目标的任何形式访问的规定,除非是指针自身作为目标的狭窄情况。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接