Является ли экземпляр шаблона класса неполным типом плохо сформированным, если тип определяется впоследствии?

Этот код, безусловно, плохо сформирован, потому что Foo специализируется после точки создания:

template <typename T>
struct Foo {
    int a;
};

Foo<int> x = { 42 };

template <>
struct Foo<int> {
    const char *a;
};

Foo<int> x = { "bar" };

Он плохо сформирован из-за той части стандарта, которую я сделал акцент:

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

Теперь, этот код плохо сформирован?

struct A;

template <typename> class Foo { };

Foo<A> foo; // note A is incomplete here

struct A {};

Изменяется ли неправильная формальность, если Foo объявлен таким образом?

struct A;

template <typename T>
struct Foo {
    Foo() {
        new T;
    }
};

Foo<A> foo; // note A is incomplete here

struct A {};

Я задал этот вопрос из-за обсуждения по этому вопросу.

Обратите внимание, что это не дубликат. Этот вопрос касается того, почему компиляция кода, этот вопрос касается того, плохо ли он сформирован. Они отличаются друг от друга, потому что плохо сформированная программа не обязательно является некомпилирующей программой.


Обратите внимание, что с clang и gcc мой пример с new T компиляторами new T, в то время как в этом примере (T как член) нет:

struct A;

template <typename T>
struct Foo {
    T t;
};

Foo<A> foo; // note A is incomplete here

struct A {};

Может быть, оба плохо сформированы, и диагностика дается только для этого последнего случая?

Ответ 1

Предполагая, что у нас есть только одна единица перевода, [temp.point] исключает вашу цитату как возможный источник плохой формальности

Специализация шаблона класса имеет не более одной точки инстанцирования внутри единицы перевода.

Вместо этого проблема с первым фрагментом - [temp.expl.spec]

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

Второй фрагмент хорошо сформирован, нет требования, чтобы параметры шаблона имели полный тип.

Третий фрагмент плохо сформирован, new T требует, чтобы T был полным типом. Небольшой улов здесь заключается в том, что определение конструктора неявно создается в Foo<A> foo; , Если, однако, фрагмент изменен на

struct A;

template <typename T>
struct Foo {
    Foo() {
        new T;
    }
};

using FooA = Foo<A>;

struct A {};

Тогда определение конструктора не создается и поэтому будет корректно сформировано. [temp.inst]

Неявное инстанцирование специализации шаблона шаблона вызывает

  • неявное создание деклараций, но не определений, функций-не-удаленных членов класса, классов-членов, облачных имен элементов, статических членов данных, шаблонов-членов и друзей; а также [...]

Четвертый фрагмент плохо сформирован, потому что члены должны иметь полный тип. [class.mem]

Тип нестатического элемента данных не должен быть неполным типом [...]

Ответ 2

struct A;
template <typename> class Foo { };
Foo<A> foo; // note A is incomplete here
struct A {};

Foo<A> зависит только от имени A не от его полного типа.

Итак, это хорошо сформировано; однако этот вид вещей все еще может сломаться (стать плохо сформированным), но скомпилировать в каждом тестируемом компиляторе.

Во-первых, мы крадем is_complete. Затем мы делаем следующее:

struct A;
template <class T> class Foo {
  enum{ value = is_complete<T>::value };
};
Foo<A> foo; // note A is incomplete here
struct A {};

Мы в порядке, несмотря на это:

[...] для любой такой специализации, которая имеет точку инстанцирования внутри единицы перевода, конец единицы перевода также считается точкой инстанцирования. [...]

потому что это предложение не относится к классам шаблонов. Здесь единственный экземпляр класса шаблона в порядке.

Теперь, если в другом файле у вас есть:

struct A {};
Foo<A> foo2;

ваша программа плохо сформирована.

Однако в случае с одним файлом:

struct A;
template <class T> class Foo {
  enum{ value = is_complete<T>::value };
};
Foo<A> foo; // note A is incomplete here
struct A {};
Foo<A> foo2; // ill-formed

ваш код в порядке. Существует одна точка инстанцирования для Foo<A> в данной единице компиляции; вторая - это ссылка на первую точку инстанцирования.

И один, и два файла versoins почти наверняка будут компилироваться в компиляторах C++ без ошибок или предупреждений.

Некоторые компиляторы запоминают экземпляры шаблонов даже из одного блока компиляции в пыльник; Foo<A> будет иметь ::value которое является false даже после создания foo2 (с полным A). Другие будут иметь два разных Foo<A> в каждом модуле компиляции; его методы будут отмечены inline (и будут отличаться), размер классов может не совпадать, и вы получите каскады плохо сформированных программных проблем.


Обратите внимание, что многие типы в std требуют, чтобы их аргументы шаблона были завершены в более старых версиях C++ (включая : "17.6.4.8 Другие функции (...) 2. эффекты не определены в следующих случаях: (...) В частности - если неполный тип (3.9) используется в качестве аргумента шаблона при создании экземпляра компонента шаблона, если только это не разрешено для этого компонента "- скопировано из документов с неполным контейнером". Чтобы быть конкретным, std::vector<T> используется для завершения T

По , который изменился для std::vector:

[Vector.overview]/3

Неполный тип T может использоваться при создании вектора, если распределитель удовлетворяет требованиям полноты распределителя 17.6.3.5.1. T должен быть завершен до того, как будет указан какой-либо элемент полученной специализации вектора.

Теперь, даже до , большинство реализаций std::vector<T> отлично с неполным T пока вы не попытаетесь использовать метод (включая многие его конструкторы или деструктор), но в стандарте указано, что T должен быть полным.

Это фактически мешает некоторому неиспользуемому коду, например, иметь тип функции, который возвращает векторы своего типа 1. У Boost есть библиотека для решения этой проблемы.


template <typename T>
struct Foo {
  Foo() {
    new T;
  }
};

Тело Foo<T>::Foo() создается только при вызове ". Таким образом, T отсутствие завершения не влияет на Foo::Foo().

Foo<A> foo;

^^ не скомпилируется с неполным A

using foo_t = Foo<A>;

^^ скомпилирует и не вызовет никаких проблем.

using foo_t = Foo<A>;
struct A {};
foo_t foo;

также нет проблем. Тело foo_t::foo_t получает экземпляр, когда мы пытаемся построить foo_t, и все определения совпадают.


1 Можете ли вы сказать, что функция перехода в состояние машины?

Ответ 3

К счастью, да, это четко определено. По той же причине это четко определено:

struct A;

class Foo { A* value; };

Foo foo; // note A is incomplete here

struct A {};

и это плохо сформировано:

struct A;

template <class T> class Foo { T value; }; // error: 'Foo<T>::value' has incomplete type

Foo<A> foo; // note A is incomplete here

struct A {};