Search code examples
c++c++14language-lawyer

How are ambiguous grammar resolved?


Is there a general rule on how to resolve ambiguous grammar?

In particular, a pseudo-destructor-name includes these production rules

nested-name-specifieropt type-name :: ~ type-name

nested-name-specifieropt ~ type-name

And nested-name-specifier includes

nested-name-specifier identifier ::

type-name ::

Given the following

struct A
{
    struct B {};
    B b;
};

A a;
a.b.A::B::~B();

Which of the following production is choosed for the last line?

type-name :: type-name :: ~ type-name

type-name :: identifier :: ~ type-name

This has significance in a templated context, since [temp.res]

In a nested-name-specifier that immediately contains a nested-name-specifier that depends on a template parameter, the identifier or simple-template-id is implicitly assumed to name a type, without the use of the typename keyword.


Solution

  • Where grammatical ambiguities occur, they should be accompanied by a disambiguation rule. Some such rules are fairly specific (e.g. [expr.unary.op]/10) while others are quite general (e.g. [dcl.ambig.res]/1). If there is no disambiguation rule that applies to a given ambiguity, the ambiguity represents an unintended defect in the standard. There is no disambiguation rule that is general enough to resolve all conceivable ambiguities.


    In the C++14 grammar, there is an ambiguity you didn't mention (pointed out by @xskxzr); it's between

        postfix-expression . templateopt id-expression

    and

        postfix-expression . pseudo-destructor-name

    (see [expr.post]/1).

    Since A::B is actually a class type, presumably this ambiguity is intended to be resolved in favour of the former. In fact, if it were interpreted as a pseudo-destructor-name, it would be ill-formed under [expr.pseudo]/2. The reason why it's called a pseudo-destructor call is that it operates on scalar types, which do not have actual destructors:

    The left-hand side of the dot operator shall be of scalar type. [..] This scalar type is the object type. The cv-unqualified versions of the object type and of the type designated by the pseudo-destructor-name shall be the same type. [...]

    There does not seem to be an applicable disambiguation rule, so this could be viewed as a defect. However, in C++20, the grammar was changed anyway, so it's moot. I will discuss this more later in the answer.


    Suppose we change your example so that it involves a scalar type:

    struct A
    {
        using C = int;
        C c;
    };
    
    A a;
    a.c.A::C::~C();
    

    Now, A::C::~C cannot be an id-expression because if it were an id-expression, it would have to be a qualified-id ([expr.prim.general]/9) consisting of a nested-name-specifier A::C:: and an unqualified-id ~C, and ~C can only be an unqualified-id if C is either a class-name or a decltype-specifier ([expr.prim.general]/1), but C is not a class-name because it has not been declared as a class ([class]/1). So A::C::~C can only be a pseudo-destructor-name, not an id-expression.

    So now the question becomes how to interpret A::C::~C as a pseudo-destructor-name. This illustrates the ambiguity that you intended to illustrate.

    The submitter of CWG 1753 seemed to think that the nested-name-specifier ~ type-name form is only intended to cover the case where the nested-name-specifier designates a namespace (not a class). The issue page indicates that this issue was voted as a DR. That means that its resolution—which was to remove the nested-name-specifier ~ type-name production—is retroactive. In other words, the committee officially eliminated this ambiguity, not by inserting an ambiguity resolution rule, but by saying that the nested-name-specifier ~ type-name interpretation never should have been legal in the first place.

    So the correct interpretation is nested-name-specifier type-name :: ~ type-name.


    In C++20, the concept of a pseudo-destructor-name was abolished altogether. This means that the ambiguity mentioned by @xskxzr was also fixed. In C++20, what used to be a pseudo-destructor-name is now just a special type of id-expression that occurs when the destructor that it denotes is a fake destructor (i.e. one of a non-class type). (For the curious, this change was made by P1131R2.)