Является ли std:: unique_ptr <T> необходимым для определения полного определения T?

У меня есть код в заголовке, который выглядит так:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Если я включаю этот заголовок в cpp, который не включает определение типа Thing, то это не компилируется под VS2010-SP1:

1 > C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include\memory (2067): ошибка C2027: использование undefined типа "Thing"

Замените std::unique_ptr на std::shared_ptr и он скомпилируется.

Итак, я предполагаю, что это текущая реализация VS2010 std::unique_ptr, которая требует полного определения и полностью зависит от реализации.

Или это? Есть ли что-то в нем стандартные требования, которые делают невозможным выполнение std::unique_ptr для работы только с форвардной декларацией? Это кажется странным, поскольку он должен содержать только указатель на Thing, не так ли?

Ответ 1

Принято здесь.

Большинство шаблонов в стандартной библиотеке С++ требуют, чтобы они были созданы с использованием всех типов. Однако shared_ptr и unique_ptr являются частичными исключениями. Некоторые, но не все их члены могут быть созданы с неполными типами. Мотивация заключается в том, чтобы поддерживать идиомы, такие как pimpl с помощью интеллектуальных указателей и без риска поведения undefined.

Undefined поведение может возникать, когда у вас есть неполный тип, и вы вызываете delete на нем:

class A;
A* a = ...;
delete a;

Вышеупомянутый законный код. Он будет компилироваться. Ваш компилятор может или не может выдавать предупреждение для вышеуказанного кода, как указано выше. Когда он исполнится, вероятно, произойдут плохие вещи. Если вам повезет, ваша программа выйдет из строя. Однако более вероятным результатом является то, что ваша программа будет без проблем протекать в памяти, поскольку ~A() не будет вызываться.

Использование auto_ptr<A> в приведенном выше примере не помогает. Вы по-прежнему получаете такое же поведение undefined, как если бы вы использовали необработанный указатель.

Тем не менее, использование неполных классов в определенных местах очень полезно! Здесь shared_ptr и unique_ptr помогают. Использование одного из этих интеллектуальных указателей позволит вам уйти с неполным типом, за исключением случаев, когда необходимо иметь полный тип. И самое главное, когда необходимо иметь полный тип, вы получаете ошибку времени компиляции, если вы пытаетесь использовать интеллектуальный указатель с неполным типом в этой точке.

Не более undefined поведение:

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

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptr и unique_ptr требуется полный тип в разных местах. Причины неясно, что связано с динамическим делетером и статическим делетером. Точные причины не важны. Фактически, в большинстве кодексов вам не обязательно знать, где именно требуется полный тип. Просто код, и если вы ошибетесь, компилятор скажет вам.

Однако, если это вам полезно, вот таблица, которая документирует несколько членов shared_ptr и unique_ptr в отношении требований полноты. Если член требует полного типа, тогда запись имеет "C", иначе запись в таблице заполняется "I".

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

Для любых операций, требующих преобразования указателей, требуются полные типы для unique_ptr и shared_ptr.

Конструктор unique_ptr<A>{A*} может уйти с неполным A, только если компилятору не требуется настраивать вызов ~unique_ptr<A>(). Например, если вы положили unique_ptr в кучу, вы можете уйти с неполным A. Более подробную информацию об этом можно найти в .

Ответ 2

Компилятору необходимо определить Thing для генерации деструктора по умолчанию для MyClass. Если вы явно объявите деструктор и переместите его (пустую) реализацию в файл CPP, код должен скомпилироваться.

Ответ 3

Это не зависит от реализации. Причина, по которой он работает, заключается в том, что shared_ptr определяет правильный деструктор для вызова во время выполнения - он не является частью сигнатуры типа. Однако деструктор unique_ptr является частью его типа и должен быть известен во время компиляции.

Ответ 4

Похоже, что текущие ответы не совсем точно объясняют, почему конструктор по умолчанию (или деструктор) является проблемой, а пустые, объявленные в cpp, - нет.

Вот что происходит:

Если внешний класс (т.е. MyClass) не имеет конструктора или деструктора, тогда компилятор генерирует классы по умолчанию. Проблема в том, что компилятор по существу вставляет пустой конструктор/деструктор по умолчанию в файл .hpp. Это означает, что код для стандартного конструктора/деструктора компилируется вместе с исполняемым двоичным файлом хоста, а не с вашими двоичными файлами библиотеки. Однако это определение не может реально создавать частичные классы. Поэтому, когда компоновщик входит в двоичный файл вашей библиотеки и пытается получить конструктор/деструктор, он не находит ничего, и вы получаете ошибку. Если код конструктора/деструктора был в вашем .cpp, то в бинарном файле вашей библиотеки он есть для связывания.

Это не имеет ничего общего с использованием unique_ptr или shared_ptr, и другие ответы, возможно, приводят к путанице в старой версии VC++ для реализации unique_ptr (VC++ 2015 отлично работает на моей машине).

Мораль этой истории в том, что ваш заголовок должен быть свободен от любого определения конструктора/деструктора. Он может содержать только их декларацию. Например, ~MyClass()=default; в hpp не будет работать. Если вы позволите компилятору вставлять конструктор или деструктор по умолчанию, вы получите ошибку компоновщика.

Еще одно замечание: если вы все еще получаете эту ошибку даже после того, как у вас есть конструктор и деструктор в файле cpp, то, скорее всего, причина в том, что ваша библиотека не компилируется должным образом. Например, однажды я просто изменил тип проекта с консоли на библиотеку в VC++ и получил эту ошибку, потому что VC++ не добавил символ препроцессора _LIB и выдает точно такое же сообщение об ошибке.

Ответ 5

Просто для полноты:

Заголовок: Ах

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

Источник A.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

Определение класса B должно просматриваться конструктором, деструктором и всем, что может косвенно удалить B. (Хотя конструктор не указан в списке выше, в VS2017 даже конструктору требуется определение B. И это имеет смысл при рассмотрении что в случае исключения в конструкторе unique_ptr снова уничтожается.)

Ответ 6

Полное определение Вещей требуется в момент создания шаблона. Это точная причина, по которой компиляция иконок pimpl компилируется.

Если бы это было невозможно, люди не задавали бы такие вопросы, как this.

Ответ 7

Простой ответ - просто используйте shared_ptr.

Ответ 8

Как по мне,

QList<QSharedPointer<ControllerBase>> controllers;

Просто включите заголовок...

#include <QSharedPointer>