17

Given the following class template:

template<typename T>
struct Outer
{
    struct Inner;

    auto f(Inner) -> void;
};

we define Inner separately for each specialization of Outer:

template<>
struct Outer<int>::Inner {};

template<>
struct Outer<double>::Inner {};

and then define the member function f once for all specializations of Outer:

auto Outer<T>::f(Inner) -> void
{

}

but Clang (9.0.0) complains:

error: variable has incomplete type 'Outer::Inner'

auto Outer<T>::f(Inner) -> void

                      ^

We can evade the compiler error by also providing a definition of Inner for all other specializations of Outer:

template<typename T>
struct Outer<T>::Inner {};

or by defining f separately for each specialization:

template<>
auto Outer<int>::f(Inner) -> void
{

}

template<>
auto Outer<double>::f(Inner) -> void
{

}

Both GCC and MSVC accept the initial code, which begs the question; is this a Clang bug or is it the only conformant implementation out of the three?

Try on Compiler Explorer

Guillaume Racicot
  • 32,627
  • 7
  • 60
  • 103
invexed
  • 573
  • 1
  • 10
  • Specialisations of Inner are irrelevant, removing them does not change the compilation result. – n. 'pronouns' m. Nov 19 '19 at 15:16
  • @n.'pronouns'm. I'm not sure what you mean. Both [adding a definition of ```Inner``` for all other specializations](https://godbolt.org/z/zRygaw) and [defining ```f``` separately for each specialization](https://godbolt.org/z/U43aMA) resolve the compilation error. – invexed Nov 19 '19 at 15:23
  • Let's read it again: *removing them does not change the compilation result*. Not adding, removing. [gcc](https://ideone.com/vVlix5) [clang](https://ideone.com/pZbUas) – n. 'pronouns' m. Nov 19 '19 at 15:31
  • @n.'pronouns'm. I see what you mean now, but that's still a strange comment to make. The point of my question was that ```Inner``` is being reported as an incomplete type despite definitions for each specialization of ```Outer``` being provided. Clearly ```Inner``` will (correctly) be an incomplete type if you remove its definition(s). – invexed Nov 19 '19 at 15:37
  • "Clearly Inner will (correctly) be an incomplete type if you remove its definition(s). " no that"s not ckear at all. A specialisation is a completely separate template and it doesn't affect the main template at all. – n. 'pronouns' m. Nov 19 '19 at 16:44
  • And by the way I'm not sure your specialisations are valid. You are specialising a member without specialising the class itself. From what I hear this is a no-no. But it is irrelevant to the code. The right question about the code is "is an incomplete type in a non-instantiated template an error". – n. 'pronouns' m. Nov 19 '19 at 16:53
  • `class Incomplete; template void foo(Incomplete){}` gcc/clang reject it, whereas msvc accepts it. (which make me think that clang is right). – Jarod42 Nov 19 '19 at 16:58
  • @n.'pronouns'm. I was under the impression that in the definition of ```Outer::f```, the completeness of ```Inner``` is not evaluated until the template is instantiated (since the type of ```Inner``` depends on the template parameter ```T```). Is that not the case? The function signature is really ```template auto Outer::f(Outer::Inner) -> void```. – invexed Nov 19 '19 at 17:07
  • That was my impression too but apparently clang disagrees. – n. 'pronouns' m. Nov 19 '19 at 17:37
  • http://eel.is/c++draft/temp.mem.class, https://stackoverflow.com/questions/27760896/nested-class-of-class-template-can-be-incompletemight might be related – Language Lawyer Nov 19 '19 at 20:00

1 Answers1

4

I believe Clang is wrong to reject your code. We must ask ourselves, how does your function declaration and definition compare to

auto f(typename T::Inner) -> void;

// ...

template<typename T>
auto Outer<T>::f(typename T::Inner) -> void
{ }

In this example, T::Inner is obviously a dependent type. So Clang may not assume it's incomplete until instantiation. Does the same hold true in your example? I would say so. For we have this in the standard:

[temp.dep.type]

5 A name is a member of the current instantiation if it is

  • An unqualified name that, when looked up, refers to at least one member of a class that is the current instantiation or a non-dependent base class thereof. [ Note: This can only occur when looking up a name in a scope enclosed by the definition of a class template.  — end note ]
  • ...

A name is a dependent member of the current instantiation if it is a member of the current instantiation that, when looked up, refers to at least one member of a class that is the current instantiation.

9 A type is dependent if it is

  • ...
  • a member of an unknown specialization,
  • a nested class or enumeration that is a dependent member of the current instantiation,
  • ...

So the first bullet in paragraph 9 covers the case typename T::Inner. That is a dependent type.

Meanwhile your case is covered by the second bullet. Outer::Inner is a name that is found in the current instantiation of Outer, moreover it's found inside Outer itself, and not in a base class. That makes it a dependent member of the current instantiation. This name refers to a nested class. Which means all the conditions in the second bullet apply, thus making Outer::Inner a dependent type as well!

Since we have ourselves a dependent type in both cases, compilers should treat them equally as dependent types. My conclusion is that GCC and MSVC are right.

StoryTeller - Unslander Monica
  • 148,497
  • 21
  • 320
  • 399