tl;dr
- This happens due to the "most vexing parse" rule
- The "most vexing parse" only considers if the code could syntactically be a declaration, but not semantically. (and in this case it is syntactically a valid function declaration;
:: in parameter names is a semantic error)
- Clang & MSVC are correct, GCC is incorrect
This answer references the C++20 Standard (N4868), unless noted otherwise.
1. The most vexing parse
The "most vexing parse" is defined by the following sections in the standard:
The important section in this case is: (emphasis mine)
9.3.3 Declarators - Ambiguity resolution [dcl.ambig.res]
(1) The ambiguity arising from the similarity between a function-style cast and a declaration mentioned in [stmt.ambig] can also occur in the context of a declaration. In that context, the choice is between a function declaration with a redundant set of parentheses around a parameter name and an object declaration with a function-style cast as the initializer. Just as for the ambiguities mentioned in [stmt.ambig], the resolution is to consider any construct that could possibly be a declaration a declaration.
Sidenote:
To be fair this paragraph is (imho) rather cryptic to read and hard to understand.
CWG2620 (which was accepted as a defect report into C++23) reworded the relevant section to make it clearer how the disambiguation should be done.
The important part is that if a given declaration could be either a function declaration or an object declaration, then it will always be a function declaration.
That is due to the statement "any construct that could possibly be a declaration [is] a declaration" — that means that any (sub-)part of the statement that could potentially be a declaration must be considered as a declaration.
In this case the relevant bit is what comes after vec — size_t(E::Count) — because that part could either be interpreted as one of the following:
- A parameter-declaration
Then the statement would be a simple-declaration declaring a function named vec with one parameter of type size_t named E::Count, returning std::vector<int*>.
i.e. syntactically equivalent to std::vector<int>* vec(size_t E::Count);
std::vector<int*> vec ( size_t ( E::Count ) ) ;
// \- decl-specifier-seq -/ | \- simple-type-specifier -/ | \- id-expression -/ | | |
// | | \- decl-specifier-seq --/ \----- declarator ---/ | |
// | | \------------ parameter-declaration ------------/ | |
// | | \------- parameter-declaration-clause --------/ | |
// | \----------------------- declarator ----------------------/ |
// \--------------------------------- simple-declaration ----------------------------------/
(Yes, E::Count is semantically not a valid name for a function parameter, we'll get to that later)
- An initializer
Then the statement would be a simple-declaration declaring a variable named vec of type std::vector<int*> that gets initialized using the initializer size_t (E::Count).
i.e. syntactically equivalent to std::vector<int*> vec((size_t)E::Count);
std::vector<int*> vec ( size_t (E::Count) ) ;
// \- decl-specifier-seq -/ | | \- initializer-list -/ | |
// | | \----- initializer -----/ |
// | \------- declarator -------/ |
// \----------------- simple-declaration ------------------/
Sidenote:
I skipped over several steps in the grammar in the examples above to keep them short.
See [gram] for the full grammar.
So size_t(E::Count) could either be interpreted as a parameter-declaration of a declarator or as the initializer of a declarator - it's ambiguous.
=> Any construct (size_t(E::Count) in this case) that could be a declaration must be a declaration (parameter-declaration is considered a "declaration").
=> The compiler must treat size_t(E::Count) as a parameter-declaration so vec will be a function declaration.
This is always a problem when the initializer of a declarator could also be interpreted as one (or more) parameter-declarations.
Declarations (like a parameter-declaration) must always be preferred - so function declarations will always win over object declarations when there is ambiguity.
CWG2620 (merged into C++23) clarifies this section a bit and explicitly mentions that parameter declarations are considered declarations: (emphasis mine)
[C++23] 9.3.3 Declarators - Ambiguity resolution [dcl.ambig.res]
(1) The ambiguity arising from the similarity between a function-style cast and a declaration mentioned in [stmt.ambig] can also occur in the context of a declaration. In that context, the choice is between an object declaration with a function-style cast as the initializer and a declaration involving a function declarator with a redundant set of parentheses around a parameter name. Just as for the ambiguities mentioned in [stmt.ambig], the resolution is to consider any construct, such as the potential parameter declaration, that could possibly be a declaration to be a declaration.
2. But why is size_t(E::Count) a valid parameter-declaration?
The C++ Standard consists out of 2 different types of rules: Syntactic and Semantic (see Difference between Syntax and Semantics).
- The syntactic rules define the grammar that C++ uses (and disambiguate ambiguous tokens) ([gram])
- The semantic rules then define what the syntactic constructs mean (and if they're even valid C++)
The disambiguation for declarations and statements happens very early on during compilation, so only syntactic rules are considered, NOT semantic ones:
8.9 Statements - Ambiguity resolution [stmt.ambig]
(3) The disambiguation is purely syntactic; that is, the meaning of the names occurring in such a statement, beyond whether they are type-names or not, is not generally used in or changed by the disambiguation. Class templates are instantiated as necessary to determine if a qualified name is a type-name. Disambiguation precedes parsing, and a statement disambiguated as a declaration may be an ill-formed declaration. If, during parsing, a name in a template parameter is bound differently than it would be bound during a trial parse, the program is ill-formed. No diagnostic is required.
So function declarations like these
std::vector<int*> vec(size_t (E::Count));
std::vector<int*> vec(size_t E::Count);
are syntactically valid C++ according to the grammar (which is all what matters when disambiguation happens)
Only after parsing are semantic rules considered (like checking if function parameter names are actually identifiers)
=> After parsing std::vector<int*> vec(size_t E::Count); will be ill-formed (because it is a function declaration with a parameter-declaration that has a qualified-id as declarator-id)
Sidenote:
Full grammatical breakdown of std::vector<int*> vec(size_t(E::Count)); : (refer to [gram])
E::Count is a ptr-declarator -> noptr-declarator -> declarator-id -> id-expression -> qualified-id -> nested-name-specifier [ E:: ] unqualified-id [ Count ]
E:: is a nested-name-specifier -> type-name :: -> enum-name -> identifier
Count is an unqualified-id -> identifier
(E::Count) is a declarator -> ptr-declarator -> noptr-declarator
[ using the ( ptr-declarator ) form, ptr-declarator is E::Count ]
size_t(E::Count) is a parameter-declaration -> decl-specifier-seq [size_t] declarator [ E::Count ]
size_t is a decl-specifier-seq -> decl-specifier -> defining-type-specifier -> type-specifier -> simple-type-specifier -> type-name -> typedef-name -> identifier
(size_t(E::Count)) is a parameters-and-qualifiers -> ( parameter-declaration-clause ) -> parameter-declaration-list -> parameter-declaration [ size_t(E::Count) ]
vec(size_t(E::Count)) is a init-declarator -> declarator -> ptr-declarator -> noptr-declarator -> noptr-declarator [ vec ] parameters-and-qualifiers [ (size_t(E::Count)) ]
vec is a noptr-declarator -> declarator-id -> id-expression -> unqualified-id -> identifier
std::vector<int*> vec(size_t(E::Count)); is a statement -> declaration-statement -> block-declaration -> simple-declaration -> decl-specifier-seq [ std::vector<int*> ] init-declarator-list [ vec(size_t(E::Count)) ]
std::vector<int*> is a decl-specifier-seq -> decl-specifier -> defining-type-specifier -> simple-type-specifier -> nested-name-specifier [ std:: ] type-name [ vector<int*> ]
std:: is a nested-name-specifier -> namespace-name :: -> identifier
vector<int*> is a type-name -> class-name -> simple-template-id -> template-name [ identifier vector ] < template-argument-list [ int* ] >
-> int* is a template-argument-list -> template-argument -> type-id -> type-specifier-seq [ int ] abstract-declarator [ * ]
int is a type-specifier-seq -> type-specifier -> simple-type-specifier -> int
* is an abstract-declarator -> ptr-abstract-declarator -> ptr-operator -> *
std::vector? Or are you declaring a function namedvec? Usingvec{ ... }(curly brckets) will fix this.auto vec = std::vector<int*>( size_t( E::Count ) );works Demosize_t( E::Count )is being treated as a function parameter instead of an expression.