¿Por qué a Clang y MSVC no les gusta una declaración typedef de miembro con un conjunto redundante de paréntesis?

Dec 05 2020

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

2 dfrib Dec 06 2020 at 20:13

Clang es incorrecto: fooen la declaración typedef en Ano 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 foodentro 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 foono 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 Aya se ha encontrado antes, y Aes 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 fooen 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 foocomo 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 fooentre paréntesis.

3 ecatmur Dec 05 2020 at 22:38

Tanto Clang como MSVC ignoran el typedefespecificador y leen la declaración como la de un constructor (es decir, Aes 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 typedefespecificador 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();
cigien Dec 05 2020 at 21:49

Estás cambiando el significado de foodesde inta 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.