here's something fun. say that x was declared in an outer scope as a typedef:
typedef int x;
now take the following code in a function body:
auto x y = 67;
this declares an automatic variable named y with type x. both gcc and clang parse this correctly. cherish this moment: it will be the last time in this blog post that gcc and clang agree on how to parse a declaration.
let's change the declaration to look like this instead:
auto x = 67;
this should (presumably) declare a variable named x with an inferred type; the new binding should shadow the typedef. and this is exactly what clang does. gcc, on the other hand,
<source>: In function 'main':
<source>:4:12: error: expected identifier or '(' before '=' token
5 | auto x = 67;
| ^
the problem is that auto can be either a storage-class specifier or a stand-in for a type specifier, depending on context. here, gcc parses x as a typedef, and so treats auto as a storage-class specifier, and therefore reports a syntax error. clang looks ahead before deciding how to treat x, so it's able to parse both declarations.
so, what's the intended behavior? is this a bug in gcc? well... it's tough to say. as far as i can tell, the standard doesn't explicitly disambiguate here.
funny enough, this same ambiguity arises in ansi c, since declarations which omit type specifiers are implicitly given type int, but ansi c explicitly disambiguates:
If the [typedef] identifier is redeclared in an inner scope or is declared as a member of a structure or union in the same or an inner scope, the type specifiers shall not be omitted in the inner declaration.
so per ansi c rules, gcc is correct to error out here. but because i can't find any explicit disambiguation in the c23 standard, my interpretation is that clang's behavior is correct, since it fits in the grammar.
and that's really funny, because it makes parsing really difficult. it might seem straightforward to just look ahead a token, but let's try throwing in some attributes:
auto x [[asdf]] [[ghjk]] y = 67;
this should declare y with type x, where x is given the attributes [[asdf]] and [[ghjk]]. this time, only gcc parses it successfully! clang gives this error message:
<source>:4:10: error: declaration of variable 'x' with deduced type 'auto' requires an initializer
5 | auto x [[asdf]] [[ghjk]] y = 67;
| ^
<source>:4:20: error: expected ';' at end of declaration
5 | auto x [[asdf]] [[ghjk]] y = 67;
| ^
| ;
clang is attempting to apply the attributes to a binding named x, and erroring out when it sees y after the attributes.
let's get rid of y and see what happens now:
auto x [[asdf]] [[ghjk]] = 67;
now it's back to gcc erroring out:
<source>: In function 'main':
<source>:4:21: error: expected identifier or '(' before '=' token
5 | auto x [[asdf]] [[ghjk]] = 67;
|
so gcc sees the attributes and tries to bind it to the type x. neither gcc nor clang backtrack later if their assumption turns out to be incorrect.
once again, the standard never explicitly disambiguates, so as far as i can tell, the "correct" behavior should be to handle both. so to parse this correctly, all attributes after the identifier need to be parsed, then another token needs to be read to decide whether or not the identifier is a type specifier, and then the attributes need to be retroactively applied to either the type or the binding depending on how the identifier is interpreted.
it gets even better though: clang supports array, function, and pointer declarators with type inference. the standard says that this is implementation-defined:
Implementations can accept a direct declarator that is not of the formoptionally enclosed in pairs of parentheses; if a direct declarator of a different form is accepted, the behavior is implementation-defined.identifier attribute-specifier-sequenceopt
it's a bit weird, because "direct declarator" excludes unparenthesized pointer declarators, which was probably an unintentional oversight. but either way, because clang chooses to support this, i'm pretty sure the following declaration is completely ambiguous:
typedef int x, y;
int main()
{
auto x (y) = 67;
}
this is either an automatic variable y with type x, or a variable named x whose type is a function accepting y and returning an inferred type (a constraint violation). clang chooses to parse it as the latter:
<source>:4:5: error: 'auto' not allowed in function return type
5 | auto x (y) = 67;
| ^~~~
<source>:4:10: error: illegal initializer (only variables can be initialized)
5 | auto x (y) = 67;
| ^
although the standard says the behavior is implementation-defined, i do think it should explicitly disambiguate here, because this is an issue with syntax, not semantics.
oh oh oh also: prior to the addition of type inference to standard c, gcc and clang both supported the extension __auto_type, which has exactly the same semantics as c23's auto.
...is what i previously thought, before going down this rabbit hole.
since __auto_type isn't a storage-class specifier, it's not exactly equivalent to auto, because the identifier will never be treated as a type specifier. and that makes sense, but it got me thinking: does __auto_type have the same scope semantics as c23 type inference?
so, ok. normally, declarations are inserted into the scope after the declarator is parsed, but before the initializer (if present). this doesn't really work as-is for type inference (or constexpr). so to accomodate this, c23 introduces the concept of "underspecified" declarations. that is, the declaration is inserted into the scope as normal after the declarator, but it doesn't have a type, so attempting to use it anywhere in the initializer is a constraint violation:
// ok: x is initialized to a poison value
int x = x;
// error: x is underspecified
auto x = x;
constexpr int x = x;
but since underspecified declarations are a new thing, does __auto_type have the same semantics? let's try it out:
int x;
int main()
{
__auto_type x = x;
}
this compiles with gcc, since gcc doesn't insert the new declaration into the scope until after the initializer is parsed. clang, on the other hand, errors out, since it uses the same semantics as c23 underspecified declarations!
interestingly, this divergence in behavior has existed for as long as __auto_type has existed in both compilers. that is, clang has always errored out here, even before type inference was standardized.
anyway, to summarize, correctly parsing type inference declarations requires:
- only deciding whether a typedef identifier is a type specifier after looking ahead for a binding,
- allowing the identifier to be followed by attributes, and retroactively applying the attributes to either the type or the binding depending on how the identifier is interpreted,
- and who the hell knows for function declarators and
__auto_type.