¿Por qué a Clang y MSVC no les gusta una declaración typedef de miembro con un conjunto redundante de paréntesis?
Considerar
using foo = int;
struct A {
typedef A (foo)();
};
GCC e ICC aceptan el fragmento, mientras que Clang y MSVC lo rechazan. El mensaje de error de Clang es
<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.
Y MSVC dice
<source>(4,15): error C2091: function returns function typedef A (foo)(); ^
( demostración en vivo )
¿Por qué Clang y MSVC producen este error? ¿Qué compiladores son correctos?
(Estoy buscando específicamente una cotización del estándar o cualquier informe de defectos).
Respuestas
Clang es incorrecto: foo
en la declaración typedef en A
no se refiere al ámbito de nombres typedef-name foo
Escriba las reglas estándar, la declaración de alias de espacio de nombres / alcance adjunto
using foo = int;
es una pista falsa; dentro del alcance declarativo de la clase A
, será sombreado por nombres declarados enA
#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>,"");
La clave aquí es que typedef A (foo)();
declara el nombre foo
dentro de la región declarativa de A
, según [dcl.spec] / 3 [ énfasis mío]:
Si se encuentra un nombre de tipo mientras se analiza un decl-specifier-seq , se interpreta como parte de decl-specifier-seq si y solo si no hay ningún especificador-tipo-definitorio anterior que no sea un calificador-cv en el decl. -specifier-seq .
Específicamente, esto significa que en la declaración typedef
typedef A (foo)();
incluso si hay un typedef-name existente foo
, que foo
no se considera en la declaración de typedef, es decir, no se considera como una parte del nombre de tipo de decl-specifier-seq de typedef A (foo)()
, como A
ya se ha encontrado antes, y A
es un especificador de tipo definitorio válido . Así, el ejemplo original:
using foo = int; struct A { typedef A (foo)(); };
se puede reducir a:
// (i)
struct A {
typedef A (foo)(); // #1
};
que declara el nombre typedef foo
en A
( A::foo
), donde la paranthese alrededor del nombre es redundante, y la declaración typedef en # 1 también se puede escribir como
// (ii)
struct A {
typedef A foo(); // #1
};
y también se puede introducir mediante una declaración de alias ( [dcl.typedef] / 2 ):
// (iii)
struct A {
using foo = A();
};
(i)
, (ii)
y (iii)
son aceptados tanto por GCC como por Clang.
Finalmente, podemos notar que Clang acepta el siguiente programa:
using foo = int;
struct A {
typedef A foo();
using bar = A();
};
static_assert(std::is_same_v<A::foo, A::bar>,"");
y que el problema raíz del ejemplo del OP es posiblemente un error de Clang, donde Clang no se adhiere a [dcl.spec] / 3 e interpreta el tipodef-name de alcance externo foo
como parte de decl-specifier-seq del declaración typedef de ámbito interno, solo para el caso en el que este último haya envuelto el nombre sombreado foo
entre paréntesis.
Tanto Clang como MSVC ignoran el typedef
especificador y leen la declaración como la de un constructor (es decir, A
es el nombre del constructor) que acepta tipos de parámetros (foo)
(es decir, (int)
) y "devuelve" un tipo de función indicado por los paréntesis finales ()
.
Sí, los constructores no tienen tipos de retorno; pero si lo hizo tener tipos de retorno que tendrían tipo de retorno A
, por lo que el adicional ()
al final hace que estos compiladores pensar que ahora tiene un constructor con el tipo de cambio, el tipo de función A()
.
Esto se respalda al señalar que las siguientes declaraciones "similares" tienen mensajes de error similares:
A (foo)();
typedef ~A(foo)();
Además, al agregar static
, podemos obtener un mensaje de error esclarecedor de MSVC:
A static (int)();
error C2574: '(__cdecl *A::A(int))(void)': cannot be declared static
Para soluciones alternativas: en Clang (pero no en MSVC) puede mover el typedef
especificador a la derecha o utilizar un especificador de tipo elaborado:
A typedef (foo)();
typedef struct A (foo)();
Debajo de todos los compiladores, puede eliminar o agregar paréntesis:
typedef A foo();
typedef A ((foo))();
Y siempre puede actualizar a un alias de tipo:
using foo = A();
Estás cambiando el significado de foo
desde int
a A()
cuando lo vuelves a declarar por dentro A
. Esto viola basic.scope.class # 2 :
Un nombre N usado en una clase S se referirá a la misma declaración en su contexto y cuando se reevalúe en el alcance completo de S. No se requiere diagnóstico para una violación de esta regla.
Dado que se trata de IFNDR, todos los compiladores cumplen.