поддержка неполного типа для списка - PullRequest
0 голосов
/ 13 апреля 2020

Я реализовал список с API-интерфейсом, аналогичным std::list, но он не компилируется

struct A { my_list<A> v; };

В списке есть базовый класс, в котором есть член base_node, который имеет prev и next полей и node (который получен из base_node) содержит значение T (которое является параметром шаблона для списка). Ошибка компиляции

error: ‘node<T>::val’ has incomplete type
     T val;
       ^~~
note: forward declaration of ‘struct A’

Я посмотрел код G CC, и похоже, что они содержат буфер байтов размера T, поэтому не уверен, как он работает для них. Как std::list удается хранить A в его узлах? enter image description here

[ОБНОВЛЕНИЕ]

struct A { };

template <typename T>
struct B : public A
{
    using B_T = B<T>;
    T t;
};

template <typename T>
class C
{
    using B_T = typename B<T>::B_T; // this fails to compile
    //using B_T = B<T>; // this compiles fine
};

struct D { C<D> d; };

1 Ответ

1 голос
/ 14 апреля 2020

В вашем упрощенном примере

struct A { };

template <typename T>
struct B : public A
{
    using B_T = B<T>;
    T t;
};

template <typename T>
class C
{
    using B_T = typename B<T>::B_T; // this fails to compile
    //using B_T = B<T>; // this compiles fine
};

struct D { C<D> d; };

вы сталкиваетесь с уловками инстанцирования шаблонов классов.

Во-первых, обратите внимание, что определение класса имеет по существу два этапа анализа (необязательно реализованных вот так):

  1. Сначала определите типы базовых классов и членов класса. Во время этого процесса класс считается неполным, хотя ранее объявленные базы и члены могут использоваться более поздним кодом в определении.

  2. В некоторых частях кода в определении класса, который не влияют на типы баз или членов, класс считается завершенным. К этим местам относятся тела определения функций-членов, аргументы по умолчанию для функций-членов, инициализаторы элементов stati c и инициализаторы по умолчанию для non-stati c.

Например:

struct S {
    std::size_t n = sizeof(S);                  // OK, S is complete

    std::size_t f() const { return sizeof(S); } // OK, S is complete

    using array_type = int[sizeof(S)];          // Error, S incomplete

    void f(int (&)[sizeof(S)]);                 // Error, S incomplete
};

Шаблоны делают это сложнее, потому что они облегчают случайное косвенное использование класса, который еще не завершен. Это особенно проявляется в коде CRTP, но этот пример - еще один простой способ, которым это может произойти.

Базовый c способ работы шаблона класса (работает немного упрощенно):

  • Простое именование специализации шаблона класса, например X<Y>, само по себе не приводит к созданию экземпляра шаблона класса.
  • Использование специализации шаблона класса способами, допустимыми для неполного типа класса, не приводит к тому, что шаблон быть экземпляром.
  • Использование специализации шаблона класса любым способом, который требует завершения типа, например, присвоение имени члену класса или определение переменной с типом класса (не указатель или ссылка), вызывает неявное создание шаблона.
  • Создание шаблона класса включает определение типов базовых классов и членов, во многом аналогично «первому этапу» анализа определения класса. Все эти базовые типы и типы участников должны быть действительными на тот момент. Создание определений элементов по большей части откладывается до тех пор, пока не понадобится каждый элемент, но на этом шаге выборочная реализация типов элементов не выполняется: все или ошибка.
  • Процесс может быть рекурсивным, когда базовый класс или член Объявление включает в себя другую специализацию шаблона. Но во время этого другого экземпляра тип класса для исходного контекста экземпляра считается неполным.

Глядя на пример, struct D определяет член C<D> d;, для выполнения которого требуется C<D>, поэтому мы пытаемся создать экземпляр специализации C<D>. Пока что D является неполным.

Есть только один член C<D>, который является

using B_T = typename B<D>::B_T;

Так как это называет члена другой специализации шаблона класса B<D>, теперь мы должны попытаться создать экземпляр этой B<D> специализации. Пока что D и C<D> все еще не завершены.

B<D> имеет один базовый класс, который является просто A. Он состоит из двух членов:

using B_T = B<D>;
D t;

Тип члена B<D>::B_T подходит, так как простое имя B<D> не требует полного типа. Но создание экземпляра B<D> требует, чтобы оба члена были хорошо сформированы. У члена класса не может быть неполного класса в качестве его типа, но тип D все еще не завершен прямо сейчас.

Как вы заметили, вы можете обойти это, избегая именования члена B<T>::B_T и непосредственно вместо этого используйте тип B<T>. Или вы можете переместить исходное определение B_T в какой-либо другой базовый класс или структуру признаков и убедиться, что его новым местоположением является такое, для которого можно создать с неполным типом в качестве аргумента.

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

...