Почему Clang и MSVC не любят объявление typedef члена с избыточным набором круглых скобок?

Dec 05 2020

Рассматривать

using foo = int;

struct A {
    typedef A (foo)();
};

GCC и ICC принимают фрагмент, а Clang и MSVC отклоняют его. Сообщение об ошибке Clang:

<source>:4:15: error: function cannot return function type 'void ()'
    typedef A (foo)();
              ^
<source>:4:13: error: typedef name must be an identifier
    typedef A (foo)();
            ^
2 errors generated.

И MSVC говорит

<source>(4,15): error C2091: function returns function
    typedef A (foo)();
              ^

( живая демонстрация )

Почему Clang и MSVC выдают эту ошибку? Какие компиляторы правильные?

(Я специально ищу цитату из стандарта или любого отчета о дефектах.)

Ответы

2 dfrib Dec 06 2020 at 20:13

Clang неверен: fooв объявлении typedef in Aне относится к области имен typedef-name foo

По стандартным правилам, включающее объявление псевдонима пространства имен / области видимости

using foo = int;

отвлекающий маневр; в декларативной области класса Aон будет затенен именами, объявленными вA

#include <type_traits>

using foo = int;
struct A {
    using foo = char;
    foo x;
};

static_assert(std::is_same_v<foo, int>,"");
static_assert(std::is_same_v<A::foo, char>,"");
static_assert(std::is_same_v<decltype(A::x), char>,"");

Ключевым моментом здесь является то, что typedef A (foo)(); объявляется имя fooв декларативной области A, согласно [dcl.spec] / 3 [ выделено мной ]:

Если имя типа встречается при синтаксическом анализе Децл-спецификатор-SEQ , он интерпретируется как часть Децла-спецификатор-SEQ тогда и только тогда , когда нет предыдущего определение типа-спецификатора кроме сорта-классификатором в ДЕЦЛЕ -specifier-seq .

В частности, это означает, что в объявлении typedef

typedef A (foo)();

даже если есть существующее ЬурейеЕ имя foo , что fooне рассматриваются в декларации ЬурейеЙ, а именно это не рассматривается как имя-типа части Децли-спецификатор-SEQ из typedef A (foo)(), так как Aуже сталкивались предыдущим к нему, и Aэто действительный спецификатор определяющего типа . Итак, оригинальный пример:

using foo = int;

struct A {
    typedef A (foo)();
};

сводится к:

// (i)
struct A {
    typedef A (foo)();  // #1
};

который объявляет имя typedef fooв A( A::foo), где скобки вокруг имени являются избыточными, а объявление typedef в # 1 также может быть записано как

// (ii)
struct A {
    typedef A foo();  // #1
};

и аналогичным образом может быть введен с помощью объявления псевдонима ( [dcl.typedef] / 2 ):

// (iii)
struct A {
    using foo = A();
};

(i), (ii)и (iii)принимаются как GCC, так и Clang.

Наконец, мы можем отметить, что Clang принимает следующую программу:

using foo = int;
struct A {
    typedef A foo();
    using bar = A();
};

static_assert(std::is_same_v<A::foo, A::bar>,"");

и что корень вопрос на пример OP, возможно , звон ошибку, где Clang не в состоянии придерживаться [dcl.spec] / 3 и интерпретирует внешнюю область действия ЬурейеГо-имя foo как часть ДЕЦЛА-спецификатор-SEQ из объявление typedef внутренней области видимости, только для случая, когда последнее заключило затененное имя fooв скобки.

3 ecatmur Dec 05 2020 at 22:38

И Clang, и MSVC игнорируют typedefспецификатор и читают объявление как объявление конструктора (то есть Aимя конструктора), принимая типы параметров (foo)(то есть (int)) и «возвращая» тип функции, обозначенный конечными круглыми скобками ().

Да, конструкторы не имеют возвращаемых типов; но если они действительно имеют типов возвращения они будут иметь тип возвращаемого значения A, поэтому дополнительные ()в конце делает эти компиляторы считают , что теперь у вас есть конструктор с типом возвращаемого типа функции A().

Это подтверждается тем, что следующие «похожие» объявления содержат похожие сообщения об ошибках:

A (foo)();
typedef ~A(foo)();

Кроме того, добавив, staticмы можем получить сообщение об ошибке от MSVC:

A static (int)();
error C2574: '(__cdecl *A::A(int))(void)': cannot be declared static

Для обходных путей: в Clang (но не в MSVC) вы можете переместить typedefспецификатор вправо или использовать разработанный спецификатор типа:

A typedef (foo)();
typedef struct A (foo)();

Под всеми компиляторами вы можете убрать или добавить круглые скобки:

typedef A foo();
typedef A ((foo))();

И вы всегда можете обновить псевдоним типа:

using foo = A();
cigien Dec 05 2020 at 21:49

Вы меняете значение fooс intна, A()когда заново объявляете его внутри A. Это нарушает basic.scope.class # 2 :

Имя N, используемое в классе S, должно ссылаться на одно и то же объявление в его контексте и при повторной оценке в завершенной области S. Диагностика не требуется для нарушения этого правила.

Поскольку это IFNDR, все компиляторы соответствуют требованиям.