5

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?

12
  • 4
    FWIW, using a C++ style cast makes it compile with everyone: gcc.godbolt.org/z/vPY4eMsY3. This looks related to the most vexing parse. Commented Mar 4 at 17:40
  • 2
    You have discovered the Most Vexing Parse. Are you creating a std::vector? Or are you declaring a function named vec? Using vec{ ... } (curly brckets) will fix this. Commented Mar 4 at 17:40
  • 1
    auto vec = std::vector<int*>( size_t( E::Count ) ); works Demo Commented Mar 4 at 17:40
  • 1
    Perhaps of note, this issue has nothing to do with enumerators. Commented Mar 4 at 17:46
  • 1
    The dupe target should explain why size_t( E::Count ) is being treated as a function parameter instead of an expression. Commented Mar 4 at 17:53

2 Answers 2

5
+50

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 vecsize_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 -> *
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. Another interesting situation is object direct-initialization with variable template. It is accepted by GCC/Clang and rejected by MSVC/EDG: gcc.godbolt.org/z/8KszqTKWT. Are both GCC and Clang incorrect there?
@Fedor I'm tempted to say that GCC and Clang are incorrect and MSVC/EDG are correct; N<int> would be a simple-template-id which is also a valid declarator-id - so it should probably also be parsed as a function declaration. But template-ids do have a few extra syntatic rules (like temp.names (2) and propably more hidden in [temp]) that might change things - I've quickly skimmed over the template section and didn't find anything unusual, so probably it should be parsed as a function declaration as well.
0

You are encountering the "most vexing parse" problem in C++.

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

The C++ parser interprets this line as a function declaration rather than the instantiation of an object. It's parsed as a function named vec returning a std::vector<int*>, and taking one unnamed parameter of type size_t with a qualified name E::Count which is illegal because parameters cannot be qualified (no scope allowed here).

Specifically, the parentheses (size_t(E::Count)) are ambiguous. To the parser, this looks like you're trying to declare a function named vec returning std::vector<int*> and taking a parameter of type size_t named E::Count. And qualified names (names with scope resolution ::) aren't allowed for parameter names, thus the error.

According to the C++ standard, to explicitly initialize an object with parentheses containing a single parameter, you must avoid ambiguity by adding extra parentheses or braces to avoid vexing parse scenarios. Therefore, MSVC and Clang correctly flag your code as invalid, while GCC accepts it errorneously.

There are several ways to fix this.

Option 1, adding extra parentheses:

std::vector<int*> vec(( size_t( E::Count ) )); // extra parentheses

Or braces (preferred)

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

GCC is incorrect in accepting the original snippet. This is a GCC bug (non-standard compliant behaviour).

11 Comments

Parameter names can't be qualified, so I don't see how this is ambiguous. Shouldn't this fall back to non-function? Note that this is a language-lawyer question, we're looking for a quote from the standard.
Thanks. Another interesting situation is object direct-initialization with variable template. It is accepted by GCC/Clang and rejected by MSVC/EDG: gcc.godbolt.org/z/8KszqTKWT. Are both GCC and Clang incorrect there?
@HolyBlackCat It is ambiguous at the time when the compiler parsed size_t and before it read anything else that follows. Parser has to make a decision at that point. This is to avoid a situation where parser reads many tokens to make decision on something way back earlier. If left unchecked it could lead to exponential complexity. Today parsers, as opposed to those from 30 years ago, are a bit better and gcc can in fact disambiguate it. But this is nonstandard.
@CygnusX1 "ambiguous at the time when the compiler parsed size_t and before it read anything else that follows. Parser has to make a decision at that point" If that was the case, then std::vector<int*> vec{ size_t(42) }; wouldn't compile either, but it does compile on all compilers. "But this is nonstandard." Citation needed.
@HolyBlackCat You mean std::vector<int*> vex(size_t(42)) (normal parenthesis). Curly braces disambiguate it immediately. Apparently it compiles. But: int i=42; std::vector<int*>(size_t(i)); does not. I thought it was related to lookahead, but now I am not so sure.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.