Pourquoi Clang et MSVC n'aiment-ils pas une déclaration typedef de membre avec un jeu redondant de parenthèses?

Dec 05 2020

Considérer

using foo = int;

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

GCC et ICC acceptent l'extrait, tandis que Clang et MSVC le rejettent. Le message d'erreur de Clang est

<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.

Et MSVC dit

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

( démo en direct )

Pourquoi Clang et MSVC produisent-ils cette erreur? Quels compilateurs sont corrects?

(Je recherche spécifiquement une citation de la norme ou de tout rapport de défaut.)

Réponses

2 dfrib Dec 06 2020 at 20:13

Clang est faux: foodans la déclaration typedef in Ane fait pas référence à la portée de l'espace de noms typedef-name foo

Wrt les règles standard, la déclaration d'alias d'espace de noms / portée englobante

using foo = int;

est un hareng rouge; dans le cadre déclarative de la classe , Ail sera assombrie par les noms déclarés dansA

#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 clé ici étant que typedef A (foo)(); déclare le nom foodans la région déclarative de A, selon [dcl.spec] / 3 [c'est moi qui souligne ]:

Si un nom de type est rencontré lors de l'analyse d'un decl-specifier-seq , il est interprété comme faisant partie du decl-specifier-seq si et seulement s'il n'y a pas de spécificateur de type de définition précédent autre qu'un qualificatif cv dans le decl -spécifier-seq .

Plus précisément, cela signifie que dans la déclaration typedef

typedef A (foo)();

même s'il existe un typedef-name existant foo, qui foon'est pas pris en compte dans la déclaration typedef, c'est-à-dire qu'il n'est pas considéré comme une partie de type-name du decl-specifier-seq de typedef A (foo)(), comme cela Aa déjà été rencontré précédemment, et Aest un spécificateur de type de définition valide . Ainsi, l'exemple original:

using foo = int;

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

peut être réduit à:

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

qui déclare le nom typedef fooentre A( A::foo), où les paranthèses autour du nom sont redondants, et la déclaration typedef en # 1 peut également être écrite comme

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

et peut également être introduit en utilisant une déclaration d'alias ( [dcl.typedef] / 2 ):

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

(i), (ii)et (iii)sont acceptés à la fois par GCC et Clang.

Enfin, nous pouvons noter que Clang accepte le programme suivant:

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

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

et que le problème racine de l'exemple de l'OP est sans doute un bogue de Clang, où Clang ne parvient pas à adhérer à [dcl.spec] / 3 et interprète le typedef-name de la portée externe foocomme faisant partie du decl-specifier-seq du déclaration typedef de portée interne, uniquement pour le cas où ce dernier a enveloppé le nom ombré fooentre parantheses.

3 ecatmur Dec 05 2020 at 22:38

Clang et MSVC ignorent tous deux le typedefspécificateur et lisent la déclaration comme celle d'un constructeur (c'est-à- Adire le nom du constructeur) acceptant les types de paramètres (foo)(c'est-à-dire (int)) et «retournant» un type de fonction indiqué par les parenthèses de fin ().

Oui, les constructeurs n'ont pas de types de retour; mais si elles ont fait avoir des types de retour , ils auraient le type de retour A, de sorte que le plus ()à la fin fait ces compilateurs pense que vous avez maintenant un constructeur avec le type de retour le type de fonction A().

Ceci est pris en charge en notant que les déclarations «similaires» suivantes ont des messages d'erreur similaires:

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

De plus, en ajoutant, staticnous pouvons obtenir un message d'erreur éclairant de MSVC:

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

Pour des solutions de contournement: sous Clang (mais pas MSVC), vous pouvez déplacer le typedefspécificateur vers la droite, ou utiliser un spécificateur de type élaboré:

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

Sous tous les compilateurs, vous pouvez supprimer ou ajouter des parenthèses:

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

Et vous pouvez toujours mettre à jour vers un alias de type:

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

Vous changez la signification de foode intà A()lorsque vous le redéclarez à l'intérieur A. Cela enfreint basic.scope.class # 2 :

Un nom N utilisé dans une classe S se référera à la même déclaration dans son contexte et lorsqu'il est réévalué dans le domaine complet de S. Aucun diagnostic n'est requis pour une violation de cette règle.

Puisqu'il s'agit d'IFNDR, tous les compilateurs sont conformes.