Нужно ли std :: unique_ptr <T> знать полное определение T?

248

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

#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: использование неопределенного типа 'Thing'

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

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

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

Klaim
источник
20
Лучшим объяснением того, когда вам нужен и не нужен полный тип с помощью интеллектуальных указателей C ++ 0x, является «Неполные типы и shared_ptr/ unique_ptr» Говарда Хиннанта . Таблица в конце должна ответить на ваш вопрос.
Джеймс Макнеллис
17
Спасибо за указатель Джеймс. Я забыл, где я положил этот стол! :-)
Говард Хиннант
5
@JamesMcNellis Ссылка на сайт Говарда Хиннанта недоступна. Вот его версия web.archive.org . В любом случае, он ответил на это совершенно ниже с тем же содержанием :-)
Ela782
Другое хорошее объяснение дано в пункте 22 Скотта Мейерса «Эффективный современный C ++».
Фред Шоен

Ответы:

328

Принято отсюда .

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

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

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

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

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

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

Нет больше неопределенного поведения:

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

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. Подробнее по этому вопросу можно найти в BarryTheHatchet в ответ здесь .

Говард Хиннант
источник
3
Отличный ответ. Я бы +5, если бы мог. Я уверен, что я вернусь к этому в моем следующем проекте, в котором я пытаюсь в полной мере использовать умные указатели.
Матиас
4
если кто-то может объяснить, что означает таблица, я думаю, это поможет большему количеству людей
Гита
8
Еще одно примечание: конструктор класса будет ссылаться на деструкторы его членов (для случая, когда выдается исключение, эти деструкторы должны быть вызваны). Таким образом, в то время как деструктору unique_ptr необходим полный тип, недостаточно иметь деструктор, определенный пользователем, в классе - ему также нужен конструктор.
Йоханнес Шауб - Litb
7
@Mehrdad: Это решение было принято для C ++ 98, что раньше моего времени. Однако я полагаю, что решение было принято из-за озабоченности по поводу реализуемости и сложности спецификации (то есть, какие именно части контейнера требуют или не требуют полного типа). Даже сегодня, с 15-летним опытом работы с C ++ 98, было бы нетривиальной задачей как ослабить спецификацию контейнера в этой области, так и убедиться, что вы не поставили вне закона важные методы реализации или оптимизации. Я думаю, что это можно сделать. Я знаю, что это будет много работы. Я знаю одного человека, делающего попытку.
Говард Хиннант
9
Поскольку это не очевидно из приведенных выше комментариев, для тех, у кого возникла эта проблема, потому что они определяют unique_ptrпеременную-член класса, просто явно объявите деструктор (и конструктор) в объявлении класса (в файле заголовка) и перейдите к их определению. в исходном файле (и поместите заголовок с полным объявлением указанного класса в исходном файле), чтобы предотвратить автоматическое встраивание компилятором конструктора или деструктора в файл заголовка (который вызывает ошибку). stackoverflow.com/a/13414884/368896 также помогает напомнить мне об этом.
Дэн Ниссенбаум
42

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

Игорь Назаренко
источник
5
Я думаю, что это прекрасная возможность использовать функцию по умолчанию. MyClass::~MyClass() = default;в файле реализации менее вероятно, что он будет случайно удален позже в будущем кем-то, кто полагает, что тело деструктора было стерто, а не намеренно оставлено пустым.
Деннис Зикефуз
@Dennis Zickefoose: К сожалению, OP использует VC ++, а VC ++ пока не поддерживает членов классов defaulted и deleted.
ildjarn
6
+1 за то, как переместить дверь в .cpp файл. Кроме того, кажется MyClass::~MyClass() = default, не перемещать его в файл реализации на Clang. (еще?)
Eonil
Вам также нужно переместить реализацию конструктора в файл CPP, по крайней мере, на VS 2017. См., Например, этот ответ: stackoverflow.com/a/27624369/5124002
jciloa
15

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

щенок
источник
8

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

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

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

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

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

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

Шиталь шах
источник
Спасибо! Это было очень лаконичное объяснение невероятно странной причуды C ++. Спасло меня много неприятностей.
JPNotADragon
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 снова уничтожается.)

Иоахим
источник
1

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

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

BЈовић
источник
-2

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

deltanine
источник
-7

Как по мне,

QList<QSharedPointer<ControllerBase>> controllers;

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

#include <QSharedPointer>
Sanbrother
источник
Ответ не связан и не имеет отношения к вопросу.
Mikus