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

Ниже приведен недопустимый код:

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; } 

Это еще раз не может скомпилировать.

Да, здесь есть специализации по шаблонам. Специализации не обязательно должны быть явными. foo - это специализация foo_impl

Barry 06.05.2018 21:42

@Barry, но тогда почему он работает с «неявной» специализацией, а не с «явной» специализацией?

gflegar 06.05.2018 21:47

Что ж, с явной специализацией это «очевидно» неправильно, но разве это не «очевидно» неправильно?

Barry 06.05.2018 21:50

@Barry, не очень "законный" аргумент :) Но я думаю, что понимаю, почему это может происходить. В вашем ответе одно из условий такого поведения: «если контекст, из которого делается ссылка на специализацию, зависит от параметра шаблона». При явной специализации контекст не зависит от параметра шаблона, поэтому правило создания экземпляра foo_impl<void>::bar непосредственно перед определением foo_impl<void> не применяется.

gflegar 06.05.2018 22:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
20
4
3 490
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

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

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

Да, мне ясно, почему у компилятора будет больше проблем с первой версией кода - но это техническая деталь компилятора. Компилятор должен делать то, что предписывает стандарт языка, а не наоборот. Этот ответ выглядит как «второй пример работает, потому что компилятор реализован определенным образом», что означает, что ответ на первый вопрос - «причуда компилятора» (в этом случае вопрос 3 не имеет значения).

gflegar 06.05.2018 20:59

@GoranFlegar: Я сказал, что я не языковой юрист. Я представил обоснование, как я это вижу.

einpoklum 06.05.2018 22:23

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

17.6.1.2 Member classes of class templates [temp.mem.class]

1 A member class of a class template may be defined outside the class template definition in which it is declared. [Note: The member class must be defined before its first use that requires an instantiation (17.8.1) For example,

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;
}

Итак, теперь, когда комментарии были удалены, я снова подчеркну, что эта цитата просто указывает где, вы можете определить bar. Он ничего не говорит о том, можете ли вы иметь член bar до того, как он будет определен (где бы он ни был определен).

Barry 07.05.2018 15:02
Ответ принят как подходящий

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

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

Non-static data members shall not have incomplete types.

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

For a class template specialization, a class member template specialization, or a specialization for a class member of a class template, if the specialization is implicitly instantiated because it is referenced from within another template specialization, if the context from which the specialization is referenced depends on a template parameter, and if the specialization is not instantiated previous to the instantiation of the enclosing template, the point of instantiation is immediately before the point of instantiation of the enclosing template. Otherwise, the point of instantiation for such a specialization immediately precedes the namespace scope declaration or definition that refers to the specialization.

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

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

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

В нем говорится: «Если специализация создается неявно, потому что на нее имеется ссылка из другого шаблона из другой специализации шаблона». Действительно ли это у нас есть? «если контекст, из которого делается ссылка на специализацию, зависит от параметра шаблона» У нас этого нет. Верный?

Rakete1111 06.05.2018 21:17

@Barry, добро пожаловать обратно! :) Единственная проблема, которую я здесь вижу, заключается в том, что нигде в моем примере нет специализации шаблона (только аргументы по умолчанию! = Специализация). Мне пришлось бы где-нибудь написать template<> foo_impl<void> { /* specialization */ };, чтобы иметь такой сценарий.

gflegar 06.05.2018 21:26

Забавно то, что когда я делать специализирую его для void, он снова не компилируется: / Я обновлю вопрос, включив это.

gflegar 06.05.2018 21:30

@Goran К сожалению, стандарт использует специализацию в нескольких контекстах. Это может сбивать с толку. vector<int> v; приводит к созданию экземпляра специализации шаблона класса vector

Barry 06.05.2018 21:37

@Barry да, не знал этого, в стандарте это четко сказано в [temp.spec] / 4. Может быть, добавьте это к своему ответу, чтобы другим людям было понятнее.

gflegar 06.05.2018 21:53

@ Rakete1111 Я начинаю думать, что у нас здесь такая ситуация: «если специализация создается неявно, потому что на нее ссылаются из другой специализации шаблона» -> на специализацию foo_impl<void>::bar ссылаются изнутри специализации foo_impl<void> (что происходит в foo{}.x.value), когда определяя переменную foo_impl<void>::x. «если контекст, из которого делается ссылка на специализацию, зависит от параметра шаблона» -> контекст (определение шаблона класса foo_impl) зависит от параметра шаблона, здесь он заменен на void.

gflegar 06.05.2018 22:37

Я (условно) приму это сейчас, так как это наиболее правдоподобное объяснение, которое у нас есть - это сделает его первым ответом, который появится и, надеюсь, подтолкнет других людей к его подтверждению / опровержению.

gflegar 06.05.2018 22:42

@Barry bar не зависит, поэтому [temp.point] не может применяться. Вы можете специализировать бар, но это не значит, что он зависимый.

Rakete1111 07.05.2018 08:45

@ Rakete1111 bar зависит от eel.is/c++draft/temp.dep#type-9.3

Barry 07.05.2018 14:47

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

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

  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:

A specialization is a class, function, or class member that is either instantiated or explicitly specialized.

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

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

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

if the context from which the specialization is referenced depends on a template parameter

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

Извините, что снова беспокою вас по этому поводу, но [temp.point] относится к зависимым именам. Ни bar, ни x не зависят. Да, вы можете их специализировать, но это не значит, что это зависит.

Rakete1111 07.05.2018 08:45

@ Rakete1111 а это не bar, а foo_impl<>::bar?

Swift - Friday Pie 07.05.2018 10:15

@Swift 3 человека здесь (включая меня) уже пытались объяснить это, см. Его ответ на его интерпретацию стандарта (в основном, «bar всегда имеет семантику пустого класса, поэтому он не зависит»).

gflegar 07.05.2018 11:47

@Swift Это не имеет значения. Я не говорю, что это всегда один и тот же bar, но просто для разных экземпляров одного и того же шаблона bar имеет одно и то же определение (но они не одинаковы). Но я перестану спорить, это непродуктивно.

Rakete1111 07.05.2018 14:18

по 3-му пункту я могу добавить, что на самом деле это не странно. При условии соблюдения всех требований к шаблонам, того факта, что порядок объявления не влияет на результат подстановки, парадигмы SFINAE и того факта, что шаблоны могут быть рекурсивными, а их реализация создается из контекста существующего кода, это единственный способ, которым шаблоны могут быть реализованы без серьезных причуды. По сути, компилятор должен выполнять отдельный проход после перевода всего модуля. Чтобы первая версия была действительной, компилятор должен выполнить два прохода, и возникает проблема с одним определением.

Swift - Friday Pie 08.05.2018 13:14

Другие вопросы по теме