C标准递归考虑一致性的原因是什么?


9

C99标准在6.5.16:2中指出:

赋值运算符的左值应为可修改的左值。

并在6.3.2.1:1中:

可修改的左值是不具有数组类型,不完整的类型,不具有const限定类型的左值,并且如果是结构或联合,则不具有任何成员(递归包括任何成员)或const限定类型的所有包含的集合或联合的元素)。

现在,让我们考虑一个非const structconst字段。

typedef struct S_s {
    const int _a;
} S_t;

按照标准,以下代码是未定义行为(UB):

S_t s1;
S_t s2 = { ._a = 2 };
s1 = s2;

这样做的语义问题是,根据实体(struct)的声明类型,应将包围实体()视为可写(非只读S_t s1),但不应通过标准措辞将其视为可写(第2条在顶部),因为const现场_a。对于读代码的程序员,该标准不清楚,该赋值实际上是一个UB,因为无法分辨出struct S_s ... S_t类型的定义是不可能的。

此外,无论如何,仅以语法方式强制执行对该字段的只读访问。不可能const将非字段的某些部分const struct真正放置到只读存储中。但是这样的标准措辞使故意丢弃const这些字段的访问程序中的字段限定符的代码无效,就像这样(对C中的结构字段进行const限定是一个好主意吗?):

(*)

#include <stdlib.h>
#include <stdio.h>

typedef struct S_s {
    const int _a;
} S_t;

S_t *
create_S(void) {
    return calloc(sizeof(S_t), 1);
}

void
destroy_S(S_t *s) {
    free(s);
}

const int
get_S_a(const S_t *s) {
    return s->_a;
}

void
set_S_a(S_t *s, const int a) {
    int *a_p = (int *)&s->_a;
    *a_p = a;
}

int
main(void) {
    S_t s1;
    // s1._a = 5; // Error
    set_S_a(&s1, 5); // OK
    S_t *s2 = create_S();
    // s2->_a = 8; // Error
    set_S_a(s2, 8); // OK

    printf("s1.a == %d\n", get_S_a(&s1));
    printf("s2->a == %d\n", get_S_a(s2));

    destroy_S(s2);
}

因此,由于某种原因,对于一个整体来说struct,它是只读的就足以声明它const

const S_t s3;

但是对于一个整体来说struct,它是非只读的,仅仅声明为w / o是不够的const

我认为会更好的是:

  1. 为了限制const具有const字段的非结构的创建,并在这种情况下发出诊断信息。可以很清楚地看到,struct包含的只读字段本身就是只读的。
  2. 定义在写入const属于非const结构的字段时的行为,以使上面的代码(*)符合标准。

否则,行为将不一致且难以理解。

那么,正如所说的那样,C Standard const递归考虑-ness 的原因是什么?


老实说,我在那里没有看到任何问题。
巴特·范·英根·谢瑙

@BartvanIngenSchenau编辑后在正文的末尾添加了该主题中所述的问题
Michael Pankov

1
为什么要下票?
Michael Pankov

Answers:


4

那么,正如所说的那样,C标准为什么要递归考虑常数性的原因是什么?

仅从类型的角度来看,不这样做是不合理的(换句话说:严重损坏并且故意不可靠)。

这是因为“ =”在结构上的含义:这是递归赋值。随之而来的是,最终您s1._a = <value>在“输入规则内”发生了事情。如果该标准允许“嵌套” const字段使用它,则它在类型系统定义中添加了严重的矛盾,这是一个明显的矛盾(也可能丢弃该const功能,因为按照其定义,它变得变得毫无用处和不可靠了)。

据我所知,您的解决方案(1)不必要将整个结构强制为const只要其字段之一为const。这样,s1._b = b对于包含的非常量._b字段上的非常量字段将是非法s1const a


好。C几乎没有声音类型的系统(更像是几年来相互绑在一起的一堆角壳)。此外,将分配分配给a的另一种方法structmemcpy(s_dest, s_src, sizeof(S_t))。而且我很确定这是实施的实际方式。在这种情况下,即使是现有的“类型系统”也不会禁止您这样做。
Michael Pankov

2
非常真实 我希望我不是要暗示C的类型系统是健全的,而只是故意使特定的语义不合理地打败了它。而且,尽管C的类型系统并未得到严格执行,但破坏它的方法通常是明确的(指针,间接访问,强制转换),即使其影响通常是隐式的且难以跟踪。因此,拥有明确的“栅栏”来破坏它们比在定义本身中具有矛盾要好。
Thiago Silva

2

原因是只读字段是只读的。没什么大惊喜。

您错误地认为唯一的影响是在ROM中的放置,当存在相邻的非常量字段时,这实际上是不可能的。实际上,优化器可以假定const未写入表达式,并基于此进行优化。当然,当存在非常量别名时,这种假设就不成立。

您的解决方案(1)违反了现有的法律和合理代码。那不会发生。您的解决方案(2)几乎消除了conston成员的含义。尽管这不会破坏现有代码,但似乎缺乏理由。


我90%确信优化程序可能不会假定const未写入字段,因为您始终可以使用memsetmemcpy,并且该字段甚至符合标准。(1)至少可以实现为附加警告,由标志启用。(2)的基本原理是,那么,恰好-没有办法的一个组件struct可以被认为是不可写的,当整个结构可写的。
Michael Pankov

“由标志确定的可选诊断”将是标准要求的唯一要求。此外,设置标志仍会破坏现有代码,因此实际上没有人会打扰该标志,这将是死胡同。至于(2),6.3.2.1:1恰好相反:只要一个组件存在,整个结构就不可写。但是,其他组件可能仍然是可写的。cf. C ++也operator=根据成员定义,因此未定义operator=何时一个成员为const。C和C ++在这里仍然兼容。
MSalters 2013年

@constantius-您可以做一些事情来故意绕过成员的常数,这一事实并不是优化程序忽略该常数的原因。您可以在函数内部丢弃constness,从而可以更改内容。但是仍然可以在调用上下文中使用优化器来假设您不会。常量对于程序员很有用,但在某些情况下,对于优化器来说也是天赐良机。
迈克尔·科恩

那么,为什么允许用ie覆盖一个不可写的结构memcpy呢?至于其他原因-好的,这是遗产,但是为什么首先要这样做呢?
Michael Pankov

1
我仍然想知道您的评论memcpy是否正确。AFACIT在另一个问题中John Bode的引用是正确的:您的代码写入了const限定的对象,因此不是讨论的标准投诉。
MSalters 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.