问题 在C语言中,在语义上是否可以创建一个不完整类型的左值?


在C89标准中,我发现了以下部分:

3.2.2.1左值和函数指示符

除非它是sizeof运算符的操作数,一元&运算符,++运算符, - 运算符或者左运算符。运算符或赋值运算符,没有数组类型的左值被转换为存储在指定对象中的值(并且不再是左值)。如果左值具有限定类型,则该值具有左值类型的非限定版本;否则该值具有左值的类型。 如果左值具有不完整的类型且没有数组类型,则行为未定义

如果我正确阅读它,它允许我们创建一个 lvalue 并在其上应用一些运算符,这些运算符在运行时编译并可能导致未定义的行为。

问题是,我想不出一个“不完整类型的左值”的例子,它可以传递编译器的语义检查和触发器 undefined behavior

考虑一个左值是

一个 左值 是指定对象的表达式(具有除void之外的对象类型或不完整类型)。

那个不完整的类型是

类型被划分为对象类型(描述对象的类型),函数类型(描述函数的类型)和 不完整的类型(描述对象但缺少确定其大小所需信息的类型)

我尝试过失败的程序:

struct i_am_incomplete;
int main(void)
{
    struct i_am_incomplete *p;
    *(p + 1);
    return 0;
}

并得到以下错误:

error: arithmetic on a pointer to an incomplete type 'struct i_am_incomplete'
    *(p + 1);
      ~ ^

任何人都可以想到这方面的例子吗? “不完整类型的左值”的示例,它可以传递编译器的语义检查和触发器 undefined behavior


更新:

正如@algrid在答案中所说,我误解了 undefined behavior, 其中包含 compile error 作为一种选择。

也许我正在分裂头发,我仍然想知道这里的潜在动机 undefined behavior 过度 disallowing an lvalue to have an incomplete type


4751
2017-09-04 01:59


起源

什么样的例子?你已经提供了一个例子。不清楚你要求的是什么。注意你的问题措辞错误。它是 语法 可能但是 语义 不可能。如果它在语法上是不可能的,那么你会遇到语法错误。 - user207421
要做任何指针运算,编译器必须知道底层数据的大小。另一方面,将指针保持为变量不需要有关数据的知识。所以,你几乎可以指定不完整​​数据类型的指针,但没有别的。 - Serge
@EJP你是对的,它应该是一个语义的东西。我正在修理它。那么,如果它在语义上是不可能的,那么这个规则的意图是什么? - soxhc
@EJP是一个“不完整类型的左值”的例子,可以通过编译器的语义检查。或者规则的意图“如果左值具有不完整的类型且没有数组类型,则行为未定义。” - soxhc
我认为OP的目标问题是:“是否有可能让一个没有约束违规的程序在不完整类型的左值上执行左值转换?” - M.M


答案:


某些构建系统可能以某种方式设计,允许以下代码:

extern struct foo x;
extern use_foo(struct foo x); // Pass by value

...
use_foo(x);

在没有编译器必须知道或关心的情况下成功处理 关于struct foo的实际表示[例如,某些系统可以通过让调用者传递一个对象的地址来处理pass-by-value,并且如果它要修改它则要求被调用的函数进行复制]。

这样的设施在可以支持它的系统上可能是有用的,我不认为标准的作者想要暗示使用该功能的代码被“破坏”,但他们也不想强制要求所有C实现支持这样的功能。使行为未定义将允许实现在实际时支持它,而不要求它们这样做。


2
2017-09-05 22:03



我认为你是对的 = y 部分;然而 x = 是一个约束违规(即使在C89中),因为赋值运算符有一个约束,左边必须是一个可修改的左值,并且“可修改的左值”的定义不包括不完整类型的左值。 - M.M
我没有看到任何使用方法 = y 在不包含约束违规的程序中的想法;由于赋值运算符约束包括如果右侧具有结构类型,则左侧必须具有兼容的结构类型 - M.M
@ M.M:按值传递不完整的结构类型怎么样?这会违反任何限制吗?此外,有许多地方标准的某些部分为某些东西做了规定,但其他部分却无法做到。该标准的作者没有试图解决主要对于学生感兴趣的无数缺陷,并且没有明显损害标准的实际用途。 - supercat
我想你就在那里: extern struct foo x; void f(); int main() { f(x); } 不会违反我能看到的任何限制。 - M.M
@ M.M:你喜欢我的编辑吗? - supercat


我相信这个程序演示了这个案例:

struct S;
struct S *s, *f();

int main(void)
{
    s = f();
    if ( 0 )
        *s;   // here
}

struct S { int x; };
struct S *f() { static struct S y; return &y; }

在标记的行上, *s 是一个不完整类型的左值,它不属于你引用的3.2.2.1(现行标准中为6.3.2.1/2)中的任何“除......”情况。因此它是未定义的行为。

我在gcc和clang中尝试了我的程序,他们都拒绝了它,错误是指向不完整类型的指针无法解除引用;但我无法在标准中找到任何会导致约束违规的地方,因此我认为编译器拒绝该程序是不正确的。或者可能通过省略这样的约束来使标准有缺陷,这是有意义的。

(因为代码在里面 if(0),这意味着编译器不能仅仅因为它是未定义的行为而拒绝它。


5
2017-09-04 03:20



虽然我倾向于认为C标准应该禁止“取消引用指向不完整类型的指针”,但我没有找到它。也许你是对的。省略此规则,标准有缺陷。同样在这种情况下,UB规则似乎是多余的。 - soxhc
为什么你认为“编译失败”不是一种有效的未定义行为? - immibis
@immibis对,我错过了那一部分。 - soxhc
@immibis编译器必须翻译程序,除非它在所有可能的代码路径中都有UB。如果它无法翻译这个程序,它就不符合要求,因为该行 *s; 无法达到(并且语言规则指定程序的其余部分必须工作) - M.M
@ChrisBeck“格式错误”是一个C ++的东西。在C中有 约束违规 这意味着编译器必须生成诊断并且可能拒绝翻译程序。还有未定义的行为不违反约束(例如 int x = 1 / argc; 当调用程序时 argc == 0)。目前 *s; 虽然我认为可能应该这样做,但这不是违反约束的行为。 - M.M


“未定义的行为”术语包括编译错误作为选项。从C89标准:

未定义的行为 - 在使用不可移植或错误的程序构造时,对错误数据或不确定值对象的行为,标准没有要求。允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行过程中以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布时)一条诊断信息)。

正如您所见,“终止翻译”是可以的。

在这种情况下,我相信您为您获得的编译错误示例代码是作为编译时错误实现的“未定义行为”的示例。


1
2017-09-04 05:07



啊哈!我明白了,所以“未定义的行为”不仅仅是运行时的不确定性。它基本上意味着“让编译器做任何他们想做的事”。在这种背景下令人困惑。 - soxhc
除非可以证明执行将到达具有未定义行为的部分,否则不能终止翻译 - M.M
@ M.M你为什么这么认为?在一般情况下很难证明这样的事情。有没有提到标准? - algrid
在一般情况下证明是不可能的,这就是为什么“未定义行为”的整个概念首先存在的原因。根据您的解释,编译器可以拒绝编译代码 if ( p == NULL ) { bar(); } else { p->foo(); } 因为 p->foo() 是什么时候未定义的行为 p == NULL。应用UB概念的唯一自洽方式是,它是由执行流程触发到达导致UB的表达式触发的。 (但如果流程确实达到了这样的表达,则效果可以“时间旅行”回来)。 - M.M
我认为标准的用意可能是允许编译器拒绝执行包含函数调用的程序,该函数调用传递的是不完整类型的参数,但我不确定符合条件的编译器是否可以在函数的情况下这样做呼叫实际上并未执行。 - supercat


当然,数组类型可以是:

extern double A[];
...
A[0] = 1;           // lvalue conversion of A

这有明确定义的行为,即使定义为 A 对编译器不可见。所以在这个TU中,数组类型永远不会完成。


1
2017-09-04 08:45



它没有显示在标题上,但我想问的是一个左值“具有不完整的类型并且没有数组类型”,根据标准,它会导致未定义的行为。 - soxhc
数组是“创建一个不完整类型的左值”的一个很好的例子。 - soxhc