5

I'm trying to define a static variable of a templated class outside of the class scope using clang:

class Bar
{
public:
    float a;
};

template<long count>
class Foo {
public:
    static Bar* test;
};

template<long count>
decltype(Foo<count>::test) Foo<count>::test; // error

int main() {
    Foo<5> f;
    return 0;
}

But I get the following error: error: redefinition of 'test' with a different type: 'decltype(Foo<count>::test)' vs 'Bar *'

It looks to me like decltype(Foo<count>::test) should evaluate to Bar *.

This code works fine on MSVC.

My question: Is there anyway to get decltype to correctly determine the type here?

In the actual code, decltype is defined in a macro that has some additional keywords depending on the configuration used, so I would like to get this working while still using decltype.

xEric_xD
  • 506
  • 1
  • 5
  • 12
  • Why can't you say `Bar *` as the type instead of `decltype(Foo::test)` in the definition of the static variable `test`? – Hari Jun 09 '23 at 08:28
  • MSVC generally has many extensions to accommodate non-standard forms of code that are not otherwise portable. – Hari Jun 09 '23 at 08:30
  • 1
    The actual code is quite complex, and we'd preferably like to keep the code as close to original as possible. If this is indeed something that's just not possible with clang though, we might have to look at alternatives (`typeof` works just fine for example) – xEric_xD Jun 09 '23 at 08:41
  • 2
    Notice than msvc also complain in C++20 mode [Demo](https://godbolt.org/z/1jWaM11z3): *"C2040: 'Foo::test': 'unknown-type' differs in levels of indirection from 'Bar *'"* – Jarod42 Jun 09 '23 at 09:11
  • `decltype(Foo::test)` based definition compiles without warning using `g++ -Werror -Wall -Wextra` and it runs as expected. – Hari Jun 09 '23 at 09:17
  • 2
    Can't you use typedef in `Foo`: [Demo](https://godbolt.org/z/9eMEGvd8v) – Jarod42 Jun 09 '23 at 09:18
  • The typedef approach looks good. With the addition of the static assert it should ensure the original code remains unchanged – xEric_xD Jun 09 '23 at 09:26
  • 1
    `static_assert(std::is_same_v::type, decltype(Foo<42>::test)>);` has better encapsulation and could suit the conditions of your code base better. – Hari Jun 09 '23 at 09:49

1 Answers1

6

This code is ill-formed, because decltype(Foo<count>::test) denotes a unique type, even though it is equivalent to Bar*.

The relevant standard sections are as follows:

Two declarations correspond if they (re)introduce the same name, both declare constructors, or both declare destructors, unless [...]

- basic.scope.scope §4

We are even shown an example that declarations don't have to be symbolically identical to correspond:

typedef int Int;
/* ... */
void f(int);                    // #1
void f(Int) {}                  // defines #1

- basic.scope.scope - example 2

However, decltype is special in this regard:

If an expression e is type-dependent, decltype(e) denotes a unique dependent type. Two such decltype-specifiers refer to the same type only if their expressions are equivalent ([temp.over.link]).

- temp.type §4

In your example:

// variable type declared as Bar* elsewhere
template<long count>
decltype(Foo<count>::test) Foo<count>::test;

decltype(Foo<count>::test) is dependent on count, and so the type is unique and different from Bar*. Proving that they are the same for arbitrary expressions is undecidable in the general case, so it makes sense that compilers disallow this.

GCC falsely compiles your example (see Compiler Explorer), but clang and MSVC don't. This is probably because GCC doesn't treat test as dependent, since it is declared with type Bar*, which is not dependent. However, that is not conforming.

Conclusion

Your best course of action is finding a way to define this out-of-line with the same type:

template<long count>
Bar* Foo<count>::test;

If you can't use Bar* directly, maybe there is another way for you to make your use of decltype not depend on count.

Alternatively, you could also define the static member inline (since C++17):

template<long count>
class Foo {
public:
    static inline Bar* test = ...;
};

Note: I've personally run into a similar issue with the compiler being unable to match out-of-line definitions of member functions to the declarations in the class.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 1
    More examples and discussion in this question: https://stackoverflow.com/questions/44294743/dependent-type-or-argument-in-decltype-in-function-definition-fails-to-compile-w – Hari Jun 09 '23 at 19:35
  • 1
    But is `Foo::test` a dependent name? A qualified name is dependent if its lookup context is dependent and is not the current instantiation ([temp.dep.type]/5.2). But `Foo` is the current instantiation ([temp.dep.type]/1.2). So it would seem that `Foo::test` is not a dependent name here, and the rule about `decltype` doesn't apply. – Brian Bi Jun 10 '23 at 01:02
  • @BrianBi `Foo` is not the current instantiation. a *template-id* can only be the current instantiation if it is **in** the definition of a class template. (See [temp.dep.type]§1]). If it was the current instantiation, we would also be allowed to write `Foo` and it would refer to `Foo`. – Jan Schultke Jun 11 '23 at 11:22
  • @JanSchultke bullet 1.2 says "in the definition of a primary class template **or a member of a primary class template**" – Brian Bi Jun 11 '23 at 14:05
  • 1
    @BrianBi we're not in the definition of the member *yet*, at least not until we have parsed the name of the declaration `Foo::test`. This is the reason why you can only refer to the current instantation `Foo` directly in member function parameter lists, trailing return types, the initializing expression of data members etc. When specifying the return type of a member function, or the type of a data member, this is not possible, because there is no current instantation. – Jan Schultke Jun 11 '23 at 15:31
  • 1
    @JanSchultke I'd be interested to see the justification for that claim based on normative wording. Note that the *injected-class-name* `Foo` can only be found **by name lookup** once the *declarator-id* has been seen; [basic.scope.class]/1 – Brian Bi Jun 12 '23 at 07:28
  • 1
    @BrianBi I'm starting to think that you're *technically* right, or at least that the standard is under-specified in this regard. However, no compiler implements it this way, in part because it requires arbitrary look-ahead parsing to know that the data member type or member function return type is looked up `Foo` instead of global scope. Not even GCC will compile this code when `Foo::test`'s type depends on `count`, only when it's independent like `Bar*` or `int`. – Jan Schultke Jun 12 '23 at 09:49