Repository navigation
CWG3160 [dcl.spec][class.ctor] Grammar ambiguity related to constructor declarations #741
Description
Activity
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
(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
staticnot being allowed for a constructor.The principle of [dcl.ambig.res] seems relevant here: interpreting
A(t4);as a constructor declaration also makes thet4part itself a declaration, while interpretingA(t4);as a non-static member declaration creates only one declaration, witht4itself 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.Reacted by S. B. TamIn the namespace and concept cases, the variable/function interpretation must be taken because
n1,n2, andc1aren't valid parameter declarations in the context-sensitive grammar.The
u1case should be disambiguated the same way as the typedef case becauseu1can 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.Reacted by S. B. TamActually, wait a second, I'm not sure how
static A(t2)();can be interpreted syntactically as a declaration ofA. You can't put two parameter lists after the declarator-id, can you?Actually, wait a second, I'm not sure how
static A(t2)();can be interpreted syntactically as a declaration ofA. 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-qualifiersThis seems to permit multiple parameters-and-qualifiers after the declarator-id.
OK, yeah, that's weird, but I guess it means an (ill-formed) declaration of a function that returns a function.
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]
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.
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.
- 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 Jens, will there be a separate issue number for what was discussed in the most recent comments by me and lprv?
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.
CWG3163 for the latter issue.
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
#1below 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])"?
My suggestion (#741 (comment)) would handle this example by disambiguating it as not a nodeclspec-function-declaration, because in
B::A(A);the unqualified-idAofB::A(if that were taken to be the declarator-id) is not the injected-class-name of its lookup context (i.e.B).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
friendnor 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.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 ifit 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 formptr-declarator
(parameter-declaration-clause)noexcept-specifieropt attribute-specifier-seqoptwhere 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: ... ]@jensmaurer bump
CWG3160 is updated.
CWG3163 is updated.
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
It's common sense that
#1is a function pointer and#4is a constructor, but the interpretation of#2and#3seems ambiguous: they could be function/variable declarations with redundant parentheses, or constructors with invalidstaticspecifiers.All compilers seem to choose the second interpretation, parsing
#2and#3as invalid constructor declarations. However, they disagree with each other ifstaticgets replaced withtypedeforfriend:GCC and EDG seem to parse
#5and#6as valid typedef declarations, and EDG seems to parse#7as 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: