19

Consider

using foo = int;

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

GCC and ICC accept the snippet, while Clang and MSVC reject it. Clang's error message is

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

And MSVC says

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

(live demo)

Why do Clang and MSVC produce this error? Which compilers are correct?

(I'm specifically looking for quotation from the standard or any defect report.)

cpplearner
  • 13,776
  • 2
  • 47
  • 72
  • 4
    Rule of thumb: If Clang and GCC disagree, Clang is correct. If MSVC and GCC disagree, GCC is correct. Since Clang and MSVC agree with each other and disagree with GCC, this is a non-transitive error! – Eric Postpischil Dec 05 '20 at 13:45
  • EDG rules them all, but not everyone has access to EDG. – Eljay Dec 05 '20 at 14:43

3 Answers3

4

Both Clang and MSVC are ignoring the typedef specifier and reading the declaration as that of a constructor (that is, A is the constructor name) accepting parameter types (foo) (that is, (int)) and "returning" a function type signified by the trailing parentheses ().

Yes, constructors don't have return types; but if they did have return types they would have return type A, so the additional () at the end makes these compilers think that you now have a constructor with return type the function type A().

This is supported by noting that the following "similar" declarations have similar error messages:

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

Also, by adding static we can get an illuminating error message from MSVC:

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

For workarounds: under Clang (but not MSVC) you can move the typedef specifier to the right, or use an elaborated type specifier:

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

Under all compilers you can remove or add parentheses:

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

And you can always update to a type alias:

using foo = A();
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • I think compilers are treating `A` as the constructor name. (`A (foo)` is a valid constructor declaration, and apparently all compilers believe that it's still a constructor declaration if it's followed by `()`.) I also believe that `A static (foo)();` is a valid declaration (as it can't be mistaken for a constructor declaration). – cpplearner Dec 05 '20 at 16:16
  • Yes, that's a good way to put it. You're right on `A static (foo)();` - it's the declaration of a static member function named `foo`. I think I can still get something useful out of a C2574 error though. – ecatmur Dec 05 '20 at 16:37
3

Clang is wrong: foo in the typedef declaration in A does not refer to the namespace-scope typedef-name foo

W.r.t. the standard rules, the enclosing namespace/scope alias declaration

using foo = int;

is a red herring; within the declarative scope of class A it will be shadowed by names declared in A

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

The key here being that typedef A (foo)(); declares the name foo within the declarative region of A, as per [dcl.spec]/3 [emphasis mine]:

If a type-name is encountered while parsing a decl-specifier-seq, it is interpreted as part of the decl-specifier-seq if and only if there is no previous defining-type-specifier other than a cv-qualifier in the decl-specifier-seq.

Specifically, this means that in the typedef declaration

typedef A (foo)();

even if there is an existing typedef-name foo, that foo is not considered in the typedef declaration, namely it is not considered as a type-name part of the decl-specifier-seq of typedef A (foo)(), as A has already been encountered previous to it, and A is a valid defining-type-specifier. Thus, the original example:

using foo = int;

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

can be reduced to:

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

which declares the typedef name foo in A (A::foo), where the paranthese around the name are redundant, and the typedef declaration at #1 can likewise be written as

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

and can likewise be introduced using an alias declaration ([dcl.typedef]/2):

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

(i), (ii) and (iii) are accepted by both GCC and Clang.

Finally, we may note that Clang accepts the following program:

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

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

and that the root issue of the example of the OP is arguably a Clang bug, where Clang fails to adhere to [dcl.spec]/3 and interprets the outer-scope typedef-name foo as part of the decl-specifier-seq of the inner-scope typedef declaration, only for the case where the latter has wrapped the shadowed name foo in parantheses.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • I agree with your analysis, though strictly speaking [dcl.spec]/3 also makes `A(size_t);` a (ill-formed) data member declaration, while all compilers treat it as a constructor declaration when `size_t` is in scope and denotes a type. But since I can't find a saner disambiguation rule, I guess that's all we have. – cpplearner Dec 06 '20 at 13:42
  • @cpplearner Just `A(size_t)` arguably falls under the special case of [\[class.mem\]/10](https://timsong-cpp.github.io/cppwp/n4659/class.mem#10), in so particularly identifying a constructor/destructor/conversion function for which [dcl.spec]/3 does not apply (as the _decl-specifier-seq_ has been omitted). It could arguably be a conflict between [class.mem]/10 and [dcl.spec]/3 when (intending to) omitting the _decl-specifier-seq_ for e.g. declaring a constructor, but any sane interpretation would let [class.mem]/10 take precedence there. When `typename` _is_ present, there is no conflict. – dfrib Dec 06 '20 at 14:01
  • @cpplearner Do you agree with my previous (comment) statement that [class.mem]/10 would govern your example of `A(size_t)`; such that there is actually a disambiguation rule in place here? – dfrib Dec 07 '20 at 10:07
  • I'm not sure. Even if it does, I think it fails to account for `explicit A(size_t)` or `constexpr A(size_t)`. (Since `explicit` and `constexpr` are *decl-specifier*s, the *decl-specifier-seq* is not omitted, and then [dcl.spec]/3 applies.) – cpplearner Dec 07 '20 at 11:46
  • @cpplearner The `explicit` case is ruled (and disambiguated) by [\[dcl.fct.spec\]](https://timsong-cpp.github.io/cppwp/n4659/dcl.fct.spec#3). The `constexpr` case is weaker, though, I agree, but [\[dcl.constexpr\]](https://timsong-cpp.github.io/cppwp/n4659/dcl.constexpr#1) does explicitly list the cases for where it _may_ be used, and a typedef declaration is not part of it, meaning both of them cannot co-exist, and in such we have (weaker/implicit) disambiguation. – dfrib Dec 07 '20 at 12:22
0

You are changing the meaning of foo from int to A() when you redeclare it inside A. This violates basic.scope.class#2:

A name N used in a class S shall refer to the same declaration in its context and when re-evaluated in the completed scope of S. No diagnostic is required for a violation of this rule.

Since this is IFNDR, all compilers are conforming.

cigien
  • 57,834
  • 11
  • 73
  • 112
  • Interesting reading, but I'd argue that (1) the name `foo` is "declared", not "used" in the class (2) this does not explain the error message given. – cpplearner Dec 05 '20 at 14:56
  • @cpplearner I think compilers are allowed to reject code that does this, even if `N` is not used. I seem to remember other cases where compilers reject code based on this rule. I'll take a look. – cigien Dec 05 '20 at 14:59
  • @cpplearner Here's a related [example](https://stackoverflow.com/questions/26681873), also [this](https://stackoverflow.com/questions/41515989), and [this](https://stackoverflow.com/questions/12943607). – cigien Dec 05 '20 at 15:08
  • [Clang accepts](https://godbolt.org/z/s1xfdj) both `typedef A foo()` and `typedef A (*foo)()`, so this doesn't seem to be the problem. Also the declaration in the original question doesn't use the previous typedef of `foo`. Another interesting thing is if you use a different type from the inclosing struct, it also works (`struct C` in the demo link). – IlCapitano Dec 05 '20 at 15:13
  • @cpplearner I might be wrong about the rule applying here, but if it does apply, listing violations that don't get rejected doesn't say much. Clang accepts `typedef A ((foo))();` for example. Is your question asking what those specific diagnostics mean? – cigien Dec 05 '20 at 15:18
  • @cigien If I understand correctly the equivalent code `using foo = A();` doesn't violate this rule. In `typedef A (foo)();` `foo` should only be an identifier, so it's previous alias to `int` shouldn't matter as it isn't used. This seems like a parsing ambiguity problem to me, especially based on the error message being _function cannot return function type_. – IlCapitano Dec 05 '20 at 15:26
  • @IlCapitano Oh, my answer is claiming that `using foo = A();` is ifndr as well. The answer could be wrong. – cigien Dec 05 '20 at 15:32
  • @cigien Based on the examples provided at [basic.scope.class#5](https://eel.is/c++draft/basic.scope.class#5) the declaration itself doesn't violate this rule, but using `foo` in class scope before the `using` alias does. Similar to `i` and `X::i` in the example. – IlCapitano Dec 05 '20 at 15:42
  • @IlCapitano _Based on the examples provided at basic.scope.class#5 the declaration itself doesn't violate this rule_ Based on the example, the declaration itself **does** violate the rule. The fact that `foo` is not "used" before redeclaration changes nothing. – Language Lawyer Dec 05 '20 at 15:45
  • @LanguageLawyer How so? There's no mention of `typedef long T;` inside `struct Y` being in violation of the rule, which is the same case here. Only `T a;` on the previous line is a problem. – IlCapitano Dec 05 '20 at 15:49
  • @IlCapitano see the last example – Language Lawyer Dec 05 '20 at 15:54
  • @LanguageLawyer But that's not what is happening here. – IlCapitano Dec 05 '20 at 15:56
  • I don't believe this applies here. In the examples you link to, e.g. `using Foo = Foo;`, the same _name_ is used as 1) a _name_ for the class-scope typedef/alias declaration, and 2) an existing _type-name_ due to a before-class declaration, and 3) a valid _defining-type-specifier_ for the class-scope typedef/alias declaration. As I've shown in my example, `foo` in the typedef declaration `typedef A (foo)();` is not considered as a _type-name_ part of the _decl-specifier-seq_ of the typedef declaration, and thus there is no changing of the meaning of `foo` (particularly, 3) does not apply). – dfrib Dec 06 '20 at 13:43
  • 2
    The passage means that you cannot declare a name outside of a class, then use it inside of a class, then declare it inside the same class to mean something else. It doesn't mean that you cannot shadow names. – n. m. could be an AI Dec 06 '20 at 14:06