Взаимозависимые классы С++, поддерживаемые std::vector

Мне было любопытно, было ли возможно создать два класса, каждый из которых содержит std::vector другого. Мое первое предположение заключалось в том, что это было бы невозможно, потому что std::vector требует полного типа, а не только прямого объявления.

#include <vector>

class B;
class A { std::vector<B> b; };
class B { std::vector<A> a; };

Я бы подумал, что объявление std::vector<B> приведет к немедленному сбою, потому что B имеет неполный тип в этой точке. Тем не менее, он успешно компилируется под gcc и clang без каких-либо предупреждений. Почему это не вызывает ошибку?

Ответ 1

T.C прокомментировал, что это фактически поведение undefined, которое рассматривается в запросе на изменение стандарта. Нарушение правила, по-видимому, [res.on.функции] 2.5:

В частности, эффекты undefined в следующих случаях: [...]

  • если неполный тип (3.9) используется в качестве аргумента шаблона при создании экземпляра компонента шаблона, если это специально не разрешено для этого компонента.

Причина, по которой это работает (и причина, по которой она может быть стандартизирована), двояка:

  • Ваша программа содержит только определения типов; объекты не создаются, вызовы не вызываются, код не генерируется. Если мы упростим это, исключив определение B (он не нужен), а затем попытайтесь создать экземпляр A, он не сработает:

    class B;
    class A { std::vector<B> b; };
    A a; // error: ctor and dtor undefined for incomplete type B.
    

    То, что также терпит неудачу, как и ожидалось, является простым

    std::vector<B> b;
    

    Причина в обоих случаях заключается в том, что компилятор должен создавать код, в отличие от простого объявления типа, которое является только грамматически релевантным.

    Использование векторного типа также хорошо в других контекстах:

    typedef std::vector<B> BVec;
    
  • Класс A можно определить, потому что, как правильно говорит Николай в своем ответе, размер std::vector<B> и, следовательно, размер члена A b не зависит от определения b (потому что вектор содержит указатель на массив элементов, а не собственный массив).

Ответ 2

Это компилируется и работает нормально, поскольку std::vector использует указатели, а не точное определение A или B. Вы можете изменить свой пример, чтобы использовать один экземпляр или массив в определении классов, подобных этому.

class B;
class A { public: B b[2]; };
class B { public: A a[2]; };

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

ошибка: поле 'b имеет неполный тип' B [2]

Однако

class B;
class A { public: B* b; };
class B { public: A* a; };

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

Также есть упрощенный пример с шаблонами

template<typename T>
struct C {
    T* t;
};

class B;
class A { public: C<B> b; };
class B { public: C<A> a; };

Это также будет работать, и, конечно же, вы можете его создать

int main() {
    B b;
    A a;
}

Ответ 3

Это из-за правил экземпляра шаблона. В точке объявления A требуется создать интерфейс std::vector<B>.

Так как интерфейс std::vector использует только указатели и ссылки на свой тип значения, а специализация в вашем примере получает экземпляр, но ни одна из его функций не используется, вы не получаете для него ошибку компилятора.

Что касается того, почему компилятор не сгенерировал A::A() — это вызвало бы ошибку, вызвав конструкцию std::vector<B> constructor — потому что ваш контекст требовал только своего объявления. Компилятор должен только генерировать значения по умолчанию для этих специальных функций-членов, если видит, что они используются.

Ссылки


& раздел; 14.7.1

[...] Неявное инстанцирование специализации шаблона шаблона вызывает неявное создание объявлений, но не определений или аргументов по умолчанию, функций-членов класса, классов-членов, облачных имен элементов, статических элементов данных и Шаблоны участников [.]

& раздел; 12

[1] [...] Реализация будет неявно объявлять эти [специальные] функции-члены для некоторых типов классов, когда программа явно не объявляет их. Реализация будет неявно определять их, если они используются odr.