返回类型能否被const限定?GCC与Clang的处理是否合规?
先看这段代码:
typedef struct { int a[1]; } S; const S foo(void) { return (S) {{3}}; } void bar(void) { int *p = foo().a; }
根据C 2018 6.2.4 8条款的规定:
具有结构体或联合体类型的非左值表达式,若该结构体或联合体包含数组类型成员(包括递归包含的所有结构体和联合体成员),则指向一个具有自动存储期和临时生命周期的对象。其生命周期始于表达式求值时,初始值为表达式的值,止于包含该表达式的完整表达式求值结束时。任何修改临时生命周期对象的尝试均会导致未定义行为。就有效类型而言,临时生命周期对象的行为如同以其值的类型声明。此类对象无需拥有唯一地址。
按照这个条款,foo()作为非左值结构体表达式,会生成一个临时对象,其有效类型应该是const S(和函数返回值类型一致)。那么foo().a这个数组成员的元素类型就是const int,取地址后得到const int *。用它去初始化int *p,违反了6.5.16.1 1的约束——目标指针类型必须包含源指针类型的所有限定符(这条赋值约束被初始化的6.7.9 11条款引用),理论上编译器应该报错。
但实际情况是,GCC和Clang都没对这个初始化操作报错;用-Wextra编译时,Clang反而会针对foo的返回类型提示:'const' type qualifier on return type has no effect。
问题解答
1. GCC和Clang的处理是否错误?
从C标准的字面定义来看,编译器的处理确实不符合预期——临时对象的类型应当继承返回值的const限定,进而数组成员的元素也应为const int,此时初始化int *p属于违反约束的行为,编译器必须给出诊断。
但编译器的实现逻辑可能是认为:函数返回的结构体/联合体是非左值,本身无法被修改(左值才能被赋值),所以顶层const对返回值没有实际作用,因此在生成临时对象时直接忽略了这个限定符。这种处理属于编译器的“实用主义”简化,但严格来说不符合标准的字面要求。
2. C标准是否规定返回类型的限定符会被忽略?
C标准没有明确规定要忽略返回类型的顶层const限定。不过需要注意的是,C语言中函数返回的结构体/联合体是非左值,而顶层const的作用是限制左值的修改操作——非左值本身就不能被赋值修改,所以返回值的顶层const在常规使用场景下确实没有实际效果。但6.2.4 8条款明确指出临时对象的有效类型“如同以其值的类型声明”,这里的“值的类型”就是函数声明的const S,所以理论上临时对象必须带有const限定。
补充场景说明
有人可能觉得这种用法毫无意义,但考虑实际场景:baz(foo().a)。数组会被隐式转换为指针传递给baz,临时对象在完整表达式结束前都是有效的。如果baz的参数是int *并尝试修改数组元素,会触发未定义行为。程序员希望通过给foo的返回类型加上const,让编译器在把临时数组的地址传递给非const指针参数时报错,从而避免意外修改临时对象的问题。
内容的提问来源于stack exchange,提问作者Eric Postpischil

