c++enumslanguage-lawyerdirect-initialization

Object direct-initialization with enumerator value converted in integer


In the following simplified program, I try to create a std::vector object sized according to an enumerator value:

#include <vector>

enum class E { Count };

int main() {
    std::vector<int*> vec( size_t( E::Count ) );
}

It works fine in GCC, but other compilers complain.

  1. Clang:
error: parameter declarator cannot be qualified
    6 |     std::vector<int*> vec( size_t( E::Count ) );
  1. MSVC:
error C2751: 'E::Count': the name of a function parameter cannot be qualified

Online demo: https://gcc.godbolt.org/z/sP3MfPGb9

Which implementation is correct here?


Solution

  • tl;dr


    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 vecsize_t(E::Count) — because that part could either be interpreted as one of the following:


    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 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 -> *