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?
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
Clang está errado: foo
na declaração de typedef em A
nã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 foo
dentro 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 foo
nã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 A
já 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 foo
em 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 foo
como parte do decl-specifier-seq do declaração typedef de escopo interno, apenas para o caso em que o último envolve o nome sombreado foo
entre parênteses.
O Clang e o MSVC estão ignorando o typedef
especificador 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 typedef
especificador 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();
Você está mudando o significado de foo
de int
para 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.