Skip to content

CWG3160 [dcl.spec][class.ctor] Grammar ambiguity related to constructor declarations #741

Description

@cpplearner

Full name of submitter (unless configured in github; will be published with the issue): Tam S. B.

Reference (section label): [dcl.spec] [class.ctor]

Link to reflector thread (if any):

Issue description:

Consider

typedef int t1, t2, t3, t4;

struct A {
    static A(*t1)(); // #1
    static A(t2)();  // #2
    static A(t3);    // #3
    A(t4);           // #4
};

It's common sense that #1 is a function pointer and #4 is a constructor, but the interpretation of #2 and #3 seems ambiguous: they could be function/variable declarations with redundant parentheses, or constructors with invalid static specifiers.

All compilers seem to choose the second interpretation, parsing #2 and #3 as invalid constructor declarations. However, they disagree with each other if static gets replaced with typedef or friend:

typedef int t5, t6, t7;

struct A {
    typedef A(t5)();  // #5
    typedef A(t6);    // #6
};

namespace ns { struct A {
    friend A(t7)();    // #7
}; }

GCC and EDG seem to parse #5 and #6 as valid typedef declarations, and EDG seems to parse #7 as a valid friend declaration. Clang and MSVC parse them as invalid constructor declarations.

MSVC also considers A static (t3); to be a constructor declaration, but no one else does.

Suggested resolution:

Activity

  1. cpplearner commented on Aug 9, 2025

    @cpplearner
    Author

    FWIW, here are cases with other kinds of context-dependent names:

    With namespace names:

    namespace n1 {};
    namespace n2 {};
    
    struct A {
        static A(n1)(); // All compilers accept
        static A(n2);   // Clang rejects; others accept
    };

    With class template name:

    template<class>
    struct u1 {};
    
    struct A {
        static A(u1)(); // All compilers reject
    };

    With concept name:

    template<class>
    concept c1 = true;
    
    struct A {
        static A(c1)(); // GCC and Clang reject; MSVC and EDG accept
    };

    demo of all above: https://godbolt.org/z/qjY9zW913

  2. t3nsor commented on Aug 9, 2025

    @t3nsor

    (3) should be disambiguated the same way as (4) because disambiguation ignores the fact that the chosen interpretation can violate a semantic constraint such as static not being allowed for a constructor.

    The principle of [dcl.ambig.res] seems relevant here: interpreting A(t4); as a constructor declaration also makes the t4 part itself a declaration, while interpreting A(t4); as a non-static member declaration creates only one declaration, with t4 itself being a mere declarator-id. However, the wording of [dcl.ambig.res] does not seem to cover this situation, so perhaps it should be amended. This would also disambiguate (2) as an ill-formed constructor declaration.

  3. t3nsor commented on Aug 9, 2025

    @t3nsor

    In the namespace and concept cases, the variable/function interpretation must be taken because n1, n2, and c1 aren't valid parameter declarations in the context-sensitive grammar.

    The u1 case should be disambiguated the same way as the typedef case because u1 can be syntactically interpreted as a CTAD placeholder ([dcl.type.simple]/3.1, [dcl.type.class.deduct]/2). It would be covered by the rule I suggested in my previous comment.

  4. t3nsor commented on Aug 9, 2025

    @t3nsor

    Actually, wait a second, I'm not sure how static A(t2)(); can be interpreted syntactically as a declaration of A. You can't put two parameter lists after the declarator-id, can you?

  5. cpplearner commented on Aug 10, 2025

    @cpplearner
    Author

    Actually, wait a second, I'm not sure how static A(t2)(); can be interpreted syntactically as a declaration of A. You can't put two parameter lists after the declarator-id, can you?

    The grammar of noptr-declarator contains:

    noptr-declarator:
        declarator-id attribute-specifier-seqopt
        noptr-declarator parameters-and-qualifiers
    

    This seems to permit multiple parameters-and-qualifiers after the declarator-id.

  6. t3nsor commented on Aug 10, 2025

    @t3nsor

    OK, yeah, that's weird, but I guess it means an (ill-formed) declaration of a function that returns a function.

  7. t3nsor commented on Oct 14, 2025

    @t3nsor

    Here's a possible resolution. Add the following subsection to the end of [class.mem]:

    Ambiguity resolution [class.mem.ambig]

    An ambiguity can arise in a member-declaration when a type-name is enclosed in parentheses. In this case, the choice is between the declaration of a function with an unnamed parameter and the declaration of a member with redundant parentheses around the declarator-id. The resolution is to consider the type-name as a simple-type-specifier rather than a declarator-id. As in [stmt.ambig], the disambiguation is purely syntactic and can result in an ill-formed interpretation.

    [Example:

    typedef int t1, t2, t3;
    template<class> struct u1;
    struct A {
      A(t1);  // OK, declares constructor having unnamed parameter of type `int`, not a data member named `t1`
      typedef A(t2);  // error: constructor declaration with `typedef`, not typedef declaration of `t2`
      friend A(t3)();  // error: declares function named `A` that returns a function with no return type, not function named `t3` that returns an `A`
      A(u1);  // error: placeholder for deduced class type cannot appear in this context ([dcl.type.class.deduct])
    };
    

    — end example]

  8. lprv commented on Oct 15, 2025

    @lprv

    This ambiguity is not specific to member declarations: in

    class A;
    A(A);

    the second line can be interpreted as either a nodeclspec-function-declaration, or a variable declaration with extra parentheses.

  9. t3nsor commented on Feb 26, 2026

    @t3nsor

    This ambiguity is not specific to member declarations: in

    class A;
    A(A);

    the second line can be interpreted as either a nodeclspec-function-declaration, or a variable declaration with extra parentheses.

    On this point, I'm inclined to say that the problem is that the grammar for nodeclspec-function-declaration is too general. The rule should be that in a declaration that could be a nodeclspec-function-declaration, if the declarator-id matches one of the forms described in [class.ctor], [class.dtor], or [class.conv.fct], then the declaration is always interpreted as a nodeclspec-function-declaration, and otherwise it's never interpreted as a nodeclspec-function-declaration.

  10. jensmaurer commented on Feb 26, 2026

    @jensmaurer
    Member
  11. changed the title [-][dcl.spec][class.ctor] Grammar ambiguity related to constructor declarations[/-] [+]CWG3160 [dcl.spec][class.ctor] Grammar ambiguity related to constructor declarations[/+] on Feb 26, 2026
  12. t3nsor commented on Mar 8, 2026

    @t3nsor

    Jens, will there be a separate issue number for what was discussed in the most recent comments by me and lprv?

  13. jensmaurer commented on Mar 10, 2026

    @jensmaurer
    Member

    I think all prospective nodeclspec-function-declarations must have a declarator-id that is a qualified-id. Maybe we can simplify the disambiguation rule to that, instead of incorporating all of [class.ctor] etc.

  14. jensmaurer commented on Mar 10, 2026

    @jensmaurer
    Member

    CWG3163 for the latter issue.

  15. cpplearner commented on Apr 7, 2026

    @cpplearner
    Author

    I think all prospective nodeclspec-function-declarations must have a declarator-id that is a qualified-id. Maybe we can simplify the disambiguation rule to that, instead of incorporating all of [class.ctor] etc.

    A variable declaration can also have a qualified-id though. All compilers treat #1 below as a valid variable declaration:

    struct A {};
    struct B : A {};
    
    B::A(A); // #1

    Maybe, instead of "if its declarator-id is a qualified-id", say something like "if the id-expression before the parentheses names a constructor ([class.qual])"?

  16. t3nsor commented on Apr 28, 2026

    @t3nsor

    My suggestion (#741 (comment)) would handle this example by disambiguating it as not a nodeclspec-function-declaration, because in B::A(A); the unqualified-id A of B::A (if that were taken to be the declarator-id) is not the injected-class-name of its lookup context (i.e. B).

  17. t3nsor commented on Jul 17, 2026

    @t3nsor

    Here's an updated version of the previous resolution for CWG3160, incorporating the feedback from the April 28 telecon to take into account some clues as to whether the declaration can declare a constructor. I would like to split off the grammar refactoring (removing the ability for a function with no return type to occur outside the top level in a declaration) into a separate issue and to not have it assigned to me (at least until I've had more time to look into it).

    Add the following subsection to the end of [class.mem]:

    Ambiguity resolution [class.mem.ambig]

    An ambiguity can arise in a member-declaration when a type-name is enclosed in parentheses. In this case, the choice is between the declaration of a function with an unnamed parameter and the declaration of a member with redundant parentheses around the declarator-id. If the former interpretation would declare a function whose name is the injected-class-name of the enclosing class or class template and its decl-specifier-seq, if present, would contain neither friend nor a decl-specifier that is not permitted for a constructor ([class.ctor.general]), the former interpretation is chosen (i.e. the type-name is considered a simple-type-specifier rather than a declarator-id); otherwise, the latter interpretation is chosen. As in [stmt.ambig], the disambiguation is purely syntactic and can result in an ill-formed interpretation.
    [Example:

    typedef int t1, t2;
    template<class> struct u1;
    struct A {
      A(t1);  // OK, declares constructor having unnamed parameter of type `int`, not a data member named `t1`
      typedef A(t2);  // OK, declares `t2` as an alias for `A`
      A(u1);  // error: placeholder for deduced class type cannot appear in this context ([dcl.type.class.deduct])
    };
    

    — end example]

    I also realized that we need an Annex C entry for the difference in parsing. In [diff.class], add after paragraph 1:

    Affected subclause: [class.mem.ambig]
    Change: In C++, certain member declarations are interpreted as constructor declarations that would be data member declarations with redundant parentheses in C.
    [Example:

    typedef int t1, A;
    struct A {
        A(t1);
    };
    int f(struct A a) {
        return a.t1;  // valid in C, invalid in C++
    }
    

    —end example]

    Rationale: C++ allows constructor declarations to use typedef-names when declaring parameter types.
    Effect on original feature: Change to semantics of well-defined feature.
    Difficulty of converting: Syntactic transformation. A data member declaration can be obtained by removing the pair of parentheses delimiting the constructor's parameter-declaration-clause.
    How widely used: Seldom.

  18. t3nsor commented on Jul 17, 2026

    @t3nsor

    For CWG3163, where we had a similar direction, here's updated drafting for [dcl.pre]/11:

    A nodeclspec-function-declaration shall declare a constructor, destructor, or conversion functionA name-declaration that has the form of a nodeclspec-function-declaration is interpreted as a nodeclspec-function-declaration if its declarator-id has one of the forms specified for the declaration of a constructor, destructor, or conversion function at namespace scope ([class.ctor.general], [class.dtor], [class.conv.fct]). Otherwise, it is never interpreted as a nodeclspec-function-declaration.

    [Note 6: Because a member function cannot be subject to a non-defining declaration outside of a class definition (11.4.2 [class.mfct]), a nodeclspec-function-declaration can only be used in a template-declaration (13.1 [temp.pre]), explicit-instantiation (13.9.3 [temp.explicit]), or explicit-specialization (13.9.4 [temp.expl.spec]).

    [Example:

    + struct A {};
    + struct B : A {};
    + B::A(A);  // OK, declares a variable named `A` having type `A`
    + B::B(A);  // error: out-of-line non-defining constructor declaration

    —end example]

    As a drive-by fix, [class.ctor.general]/1 should be edited so that any declaration where the declarator-id has one of the specified forms is always considered a (possibly ill-formed) constructor declaration. This brings it in line with [class.dtor] and [class.conv.fct] and justifies the striking of the sentence above:

    A declaratordeclaration declares a constructor if it is a function declarator ([dcl.fct]) of

    • in a friend declaration ([class.friend]), the declarator-id is a qualified-id that names a constructor ([class.qual]);
    • otherwise, in a member-declaration that belongs to the member-specification of a class or class template, the declarator-id is the injected-class-name ([class.pre]) of the immediately-enclosing entity;
    • otherwise, the declarator-id is a qualified-id whose unqualified-id is the injected-class-name of its lookup context.

    itsThe declarator of a constructor declaration shall have the form

    ptr-declarator ( parameter-declaration-clause ) noexcept-specifieropt attribute-specifier-seqopt

    where the ptr-declarator consists solely of an id-expression, an optional attribute-specifier-seq, and optional surrounding parentheses. and the id-expression has one of the following forms:

    • in a friend declaration ([class.friend]), the id-expression is a qualified-id that names a constructor ([class.qual]);
    • otherwise, in a member-declaration that belongs to the member-specification of a class or class template, the id-expression is the injected-class-name ([class.pre]) of the immediately-enclosing entity;
    • otherwise, the id-expression is a qualified-id whose unqualified-id is the injected-class-name of its lookup context.

    Constructors do not have names. In a constructor declaration, each decl-specifier in the optional decl-specifier-seq shall be friend, inline, constexpr, consteval, or an explicit-specifier. [Example 1: ... ]

  19. t3nsor commented on Jul 26, 2026

    @t3nsor
  20. jensmaurer commented on Oct 5, 2026

    @jensmaurer
    Member

    CWG3160 is updated.

  21. t3nsor commented on Oct 5, 2026

    @t3nsor

    CWG3160 is updated.

    There's drafting for CWG3163 above too

  22. jensmaurer commented on Oct 6, 2026

    @jensmaurer
    Member

    CWG3163 is updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions