Por que o Clang e o MSVC não gostam de uma declaração de typedef de membro com um conjunto redundante de parênteses?

Dec 05 2020

Considerar

using foo = int;

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

GCC e ICC aceitam o snippet, enquanto Clang e MSVC o rejeitam. A mensagem de erro do 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.

E MSVC diz

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

( demonstração ao vivo )

Por que o Clang e o MSVC geram esse erro? Quais compiladores estão corretos?

(Estou procurando especificamente uma cotação do padrão ou qualquer relatório de defeito.)

Respostas

2 dfrib Dec 06 2020 at 20:13

Clang está errado: foona declaração de typedef em Anão se refere ao escopo do namespace typedef-name foo

Por meio das regras padrão, a declaração de namespace / escopo de alias

using foo = int;

é um arenque vermelho; dentro do escopo declarativo da classe A, será sombreado por nomes declarados emA

#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>,"");

A chave aqui é que typedef A (foo)(); declara o nome foodentro da região declarativa de A, conforme [dcl.spec] / 3 [ ênfase minha]:

Se um nome de tipo for encontrado durante a análise de um decl-especificador-seq , ele será interpretado como parte do decl-especificador-seq se e somente se não houver nenhum especificador de tipo de definição anterior diferente de um qualificador cv no decl -specifier-seq .

Especificamente, isso significa que na declaração de typedef

typedef A (foo)();

mesmo se houver um nome de typedef existente foo, que foonão é considerado na declaração de typedef, ou seja, não é considerado como uma parte do nome-tipo do decl-especificador-seq de typedef A (foo)(), como Ajá foi encontrado antes dele, e Aé um especificador de tipo de definição válido . Assim, o exemplo original:

using foo = int;

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

pode ser reduzido a:

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

que declara o nome do typedef fooem A( A::foo), onde os parantheses ao redor do nome são redundantes, e a declaração do typedef em # 1 também pode ser escrita como

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

e também pode ser introduzido usando uma declaração de alias ( [dcl.typedef] / 2 ):

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

(i), (ii)e (iii)são aceitos pelo GCC e pelo Clang.

Por fim, podemos observar que o Clang aceita o seguinte programa:

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

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

e que o problema raiz do exemplo do OP é indiscutivelmente um bug do Clang, onde o Clang falha em aderir a [dcl.spec] / 3 e interpreta o typedef-name do escopo externo foocomo parte do decl-specifier-seq do declaração typedef de escopo interno, apenas para o caso em que o último envolve o nome sombreado fooentre parênteses.

3 ecatmur Dec 05 2020 at 22:38

O Clang e o MSVC estão ignorando o typedefespecificador e lendo a declaração como a de um construtor (ou seja, Aé o nome do construtor) aceitando tipos de parâmetro (foo)(ou seja, (int)) e "retornando" um tipo de função representado pelos parênteses finais ().

Sim, os construtores não têm tipos de retorno; mas se eles fizeram têm tipos de retorno que teriam tipo de retorno A, de modo que o adicional ()no final faz com que esses compiladores acho que agora você tem um construtor com tipo de retorno o tipo de função A().

Isso é suportado observando que as seguintes declarações "semelhantes" têm mensagens de erro semelhantes:

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

Além disso, ao adicionar static, podemos obter uma mensagem de erro esclarecedora do MSVC:

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

Para soluções alternativas: em Clang (mas não MSVC), você pode mover o typedefespecificador para a direita ou usar um especificador de tipo elaborado:

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

Em todos os compiladores, você pode remover ou adicionar parênteses:

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

E você sempre pode atualizar para um alias de tipo:

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

Você está mudando o significado de foode intpara A()quando redeclarifica-o internamente A. Isso viola basic.scope.class # 2 :

Um nome N usado em uma classe S deve referir-se à mesma declaração em seu contexto e quando reavaliado no escopo completo de S. Nenhum diagnóstico é necessário para uma violação desta regra.

Como se trata de IFNDR, todos os compiladores estão em conformidade.