Недопустимый тип не допускается в классе, но разрешен в шаблоне класса

Недопустимый код:

struct foo {
    struct bar;
    bar x;        // error: field x has incomplete type
    struct bar{ int value{42}; };
};

int main() { return foo{}.x.value; }

Это совершенно очевидно, так как foo::bar считается неполным в точке, где foo::x определен.

Однако, похоже, существует "обходной путь", который делает то же определение класса действительным:

template <typename = void>
struct foo_impl {
    struct bar;
    bar x;        // no problems here
    struct bar{ int value{42}; };
};

using foo = foo_impl<>;

int main() { return foo{}.x.value; }

Это работает со всеми основными компиляторами. У меня есть три вопроса:

  1. Действительно ли это действительно C++ код или просто причуда компиляторов?
  2. Если это допустимый код, существует ли в стандарте C++ абзац, касающийся этого исключения?
  3. Если это допустимый код, почему первая версия (без template) считается недействительной? Если компилятор может определить второй вариант, я не вижу причин, по которым он не смог бы определить первый.

Если я добавлю явную специализацию для void:

template <typename = void>
struct foo_impl {};

template<>
struct foo_impl<void> {
    struct bar;
    bar x;        // error: field has incomplete type
    struct bar{ int value{42}; };
};

using foo = foo_impl<>;

int main() { return foo{}.x.value; } 

Это еще раз не скомпилируется.

Ответ 1

Реальный ответ может быть ¯\_ (ツ) _/¯, но он, вероятно, в настоящее время в порядке, потому что шаблоны являются волшебными, но может быть более явно не в порядке до некоторых других основных решений проблемы.

Во-первых, основной проблемой, конечно, является [class.mem]/14:

Нестатические данные не должны иметь неполных типов.

Вот почему ваш пример без шаблонов плохо сформирован. Однако, согласно [temp.point]/4:

Для специализации шаблона класса, специализации шаблона члена класса или специализации для члена класса шаблона класса, если специализация неявно создается, потому что на нее ссылаются из другой специализации шаблона, если контекст, с которого ссылается специализация, зависит в параметре шаблона, и если специализация не создается до создания экземпляра закрывающего шаблона, точка инстанцирования происходит непосредственно перед точкой инстанцирования охватывающего шаблона. В противном случае точка экземпляра для такой специализации сразу предшествует объявлению или определению области пространства имен, которое относится к специализации.

Это указывает на то, что foo_impl<void>::bar foo_impl<void> до foo_impl<void> и, следовательно, завершается в точке, где экземпляр нестатических данных типа bar создается. Так что, может быть, все в порядке.

Тем не менее, проблемы 1626 и 2335 основного языка касаются не совсем то же, но все еще довольно похожих вопросов, касающихся полноты и шаблонов, и оба указывают на то, чтобы сделать случай шаблона более согласованным с случаем, отличным от шаблона.

Что все это значит, если рассматривать его в целом? Я не уверен.

Ответ 2

Я думаю, что этот пример явно разрешен

17.6.1.2. Классы классов шаблонов классов [temp.mem.class]

1 Класс-член шаблона класса может быть определен за пределами определения шаблона класса, в котором он объявлен. [Примечание. Класс участника должен быть определен до его первого использования, для которого требуется экземпляр (17.8.1). Например,

template<class T> struct A {
  class B;
};

A<int>::B* b1; // OK: requires A to be defined but not A::B
template<class T> class A<T>::B { };
A<int>::B b2; // OK: requires A::B to be defined

-End note]

Это тоже должно хорошо работать:

template <typename = void>
struct foo_impl {
    struct bar;
    bar x;        // no problems here
};

template<typename T>
struct foo_impl<T>::bar{ int value{42}; };

using foo = foo_impl<>;

int main()
{
    return foo{}.x.value;
}

Ответ 3

Подробнее о принятом ответе

Я не уверен, что принятый ответ - правильное объяснение, но сейчас это наиболее правдоподобно. Экстраполируя из этого ответа, вот ответы на мои оригинальные вопросы:

  1. Действительно ли это действительно C++ код или просто причуда компиляторов? [ Это действительный код. ]
  2. Если это допустимый код, существует ли в стандарте C++ абзац, касающийся этого исключения? [ [temp.point]/4 ]
  3. Если это допустимый код, почему первая версия (без template) считается недействительной? Если компилятор может определить второй вариант, я не вижу причин, по которым он не смог бы определить первый. [ Потому что C++ странно - он обрабатывает шаблоны классов по-разному, чем классы (возможно, вы, наверное, догадались об этом). ]

Еще несколько объяснений

Что, кажется, происходит

При создании экземпляра foo{} в main компилятор создает (неявную) специализацию для foo_impl<void>. Эта специализация ссылается на foo_impl<void>::bar в строке 4 (bar x;). Контекст находится в определении шаблона, поэтому он зависит от параметра шаблона, и специализация foo_impl<void>::bar, очевидно, ранее не была создана, поэтому все предварительные условия для [temp.point]/4 выполняются, и компилятор генерирует следующий промежуточный (псевдо) код:

template <typename = void>
struct foo_impl {
    struct bar;
    bar x;        // no problems here
    struct bar{ int value{42}; };
};

using foo = foo_impl<>;

// implicit specialization of foo_impl<void>::bar, [temp.point]/4
$ struct foo_impl<void>::bar {
$     int value{42};
$ };
// implicit specialization of foo_impl<void> 
$ struct foo_impl<void> {
$     struct bar;
$     bar x;   // bar is not incomplete here
$ };
int main() { return foo{}.x.value; }

О специализации

Согласно [temp.spec]/4:

Специализация - это класс, функция или член класса, который либо создается, либо явно специализирован.

поэтому вызов foo{}.x.value в исходной реализации с шаблонами квалифицируется как специализация (это было что-то новое для меня).

О версии с явной специализацией

Версия с явной специализацией не компилируется, поскольку кажется, что:

если контекст, с которого ссылается специализация, зависит от параметра шаблона

больше не выполняется, поэтому правило от [temp.point]/4 не применяется.

Ответ 4

Я отвечу на третью часть вашего вопроса - как IANALL (не юрист по языку).

Код недействителен по той же причине, что он недействителен для использования функции до того, как она была объявлена, хотя компилятор может выяснить, что должна быть функция, идя дальше вниз в одной и той же единицы перевода. И случаи аналогичны также в том смысле, что если у вас есть только объявление без определения, которое достаточно хорошо для компилятора, в то время как здесь у вас есть определение шаблона до создания экземпляра.

Итак, дело в следующем: языковой стандарт требует, чтобы компилятор не смотрел в будущее, когда вы хотите что-то определить (а шаблон класса не является определением класса).