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

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

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

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


person Klaim    schedule 16.05.2011    source источник
comment
Лучшее объяснение того, когда вам нужен и не нужен полный тип с интеллектуальными указателями C ++ 0x, - это Неполные типы и _1 _ / _ 2_ Таблица в конце должна ответить на ваш вопрос.   -  person James McNellis    schedule 16.05.2011
comment
Спасибо за указатель Джеймс. Я забыл, где я поставил этот стол! :-)   -  person Howard Hinnant    schedule 16.05.2011
comment
stackoverflow.com/a/49187113/4361073   -  person parasrish    schedule 09.03.2018
comment
@JamesMcNellis Ссылка на сайт Говарда Хиннанта не работает. Вот веб-архив. org. Во всяком случае, он ответил на него ниже точно с тем же содержанием :-)   -  person Ela782    schedule 12.05.2018
comment
Еще одно хорошее объяснение дано в пункте 22 книги Скотта Мейерса «Эффективный современный C ++».   -  person Fred Schoen    schedule 19.07.2018


Ответы (9)


Взято из здесь.

Большинство шаблонов в стандартной библиотеке 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 здесь.

person Howard Hinnant    schedule 22.05.2011
comment
Я добавил конструкторы в свои классы, и теперь они работают с прямым объявлением Mytype, когда я использую unique_ptr ‹MyType›. Спасибо! - person Klaim; 16.06.2011
comment
Отличный ответ. Я бы +5, если бы мог. Я уверен, что вернусь к этому в моем следующем проекте, в котором я пытаюсь в полной мере использовать интеллектуальные указатели. - person matthias; 07.01.2012
comment
если можно объяснить, что означает таблица, я думаю, это поможет большему количеству людей - person Ghita; 02.05.2012
comment
Еще одно замечание: конструктор класса будет ссылаться на деструкторы своих членов (в случае возникновения исключения эти деструкторы необходимо вызвать). Таким образом, хотя деструктору unique_ptr нужен полный тип, недостаточно иметь в классе деструктор, определенный пользователем - ему также нужен конструктор. - person Johannes Schaub - litb; 02.05.2012
comment
@ JohannesSchaub-litb: Согласен. Я надеялся, что конструктору noexcept не нужно будет ссылаться на деструкторы, как вы правильно указали, но похоже, что это не так. - person Howard Hinnant; 03.05.2012
comment
@ HowardHinnant, я не знаю, как обстоят дела по этому поводу. Недавно об этом говорили в контексте доступности деструкторов подобъектов изнутри конструкторов. Похоже, было решено, что деструкторы подобъектов всегда должны быть доступны (наихудший сценарий). Я предполагаю, что то же самое и с требованием их экземпляров. - person Johannes Schaub - litb; 03.05.2012
comment
В общем, если вы используете идиому pimpl, unique_ptr работает только с классом / структурой POD? Если вы все же определяете конструктор, вам понадобится shared_ptr? - person Zoomulator; 12.05.2012
comment
@Zoomulator: Нет. Вы можете использовать unique_ptr для pimpl. Вам просто нужно выделить ваших особых членов. И это безопасно, потому что, если вы случайно забудете, компилятор вам напомнит. Вы случайно не попадете в неопределенное поведение во время выполнения, как это было с auto_ptr в C ++ 98/03. - person Howard Hinnant; 12.05.2012
comment
Для тех, кому интересно, эта возможность указана в стандарте в следующих местах: [unique.ptr]/5 для unique_ptr и [util.smartptr.shared]/2 shared_ptr. unique_ptr должен иметь полный тип везде, где он вызывает default_delete::operator() из-за [unique.ptr.dltr.dflt]/4. - person Mankarse; 01.05.2013
comment
-1 Связанная статья (и, следовательно, ответ) не объясняет почему unique_ptr и shared_ptr имеют разные требования. - person Nikolai; 07.05.2013
comment
@HowardHinnant: Какова причина того, что контейнеры C ++ требуют полных типов? Вполне возможно избежать этого (Boost.Container делает это - контейнерам не нужно заранее знать размеры своих целей), так почему же произвольное требование? - person user541686; 01.12.2013
comment
@Mehrdad: Это решение было принято для C ++ 98, до меня. Однако я считаю, что решение было принято из-за озабоченности по поводу реализуемости и сложности спецификации (то есть, какие именно части контейнера требуют или не требуют полного типа). Даже сегодня, имея 15-летний опыт работы с C ++ 98, было бы нетривиальной задачей как ослабить спецификацию контейнера в этой области, так и убедиться, что вы не ставите вне закона важные методы реализации или оптимизации. Я думаю, что это можно сделать. Я знаю, что это будет много работы. Мне известно об одном человеке, предпринимавшем попытку. - person Howard Hinnant; 01.12.2013
comment
@HowardHinnant: Понятно. Теоретически я думаю, что это должно быть возможно, потому что объекты-контейнеры содержат только указатели, а это означает, что неполные типы должны работать нормально. На практике я могу представить, что вы столкнетесь с проблемами. Спасибо за объяснение! - person user541686; 02.12.2013
comment
Я думаю, что конструктор unique_ptr также оказывается C в этой таблице. См. здесь. - person Barry; 16.03.2016
comment
@Barry: Ваши утверждения о безопасности исключений верны. Но если вы создаете unique_ptr таким образом, что компилятор не установил вызов деструктора, конструктор T* может работать с неполным T. Одним из примеров может быть помещение unique_ptr в кучу. - person Howard Hinnant; 16.03.2016
comment
@HowardHinnant Верно. Может быть, стоит добавить сноску к вашему ответу, так как на него есть ссылки отовсюду? - person Barry; 16.03.2016
comment
Поскольку это не очевидно из приведенных выше комментариев, для тех, у кого есть эта проблема, потому что они определяют unique_ptr как переменную-член класса, просто явно объявите деструктор (и конструктор) в объявлении класса (в заголовочный файл) и перейдите к определению их в исходном файле (и поместите заголовок с полным объявлением указанного класса в исходный файл), чтобы предотвратить автоматическое встраивание конструктора или деструктора компилятором в файле заголовка (что вызывает ошибку). stackoverflow.com/a/13414884/368896 также помогает мне об этом напомнить. - person Dan Nissenbaum; 07.03.2018
comment
Так что я все еще немного сбит с толку. У меня есть класс, у которого есть метод, возвращающий null unique_ptr ‹T›. Но похоже, что ему нужно знать полный тип T. Так что я не уверен, что происходит. - person ksb; 21.06.2018
comment
Обратите внимание на мой собственный болезненный опыт, связанный с этой темой: инициализация уникального указателя по умолчанию (т.е. std::unique_ptr<Thing> my_thing{};) также предотвращает прямое объявление класса. Решение простое: не инициализируйте уникальный указатель по умолчанию. - person Roland Sarrazin; 16.09.2019
comment
@RolandSarrazin Просто был такой же опыт. Было довольно плохо, так как я не мог воспроизвести его: wandbox.org/permlink/rmmbeU49lDBwTsNV. После смены версии компилятора это стало возможным, поскольку GCC9. Однако последняя версия Clang 10 не работает. - person typ1232; 19.11.2019
comment
@ typ1232: это компилируется с помощью clang: wandbox.org/permlink/MnEXt9sw4NIZLiWW - person Howard Hinnant; 19.11.2019

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

person Igor Nazarenko    schedule 16.05.2011
comment
Я думаю, что это прекрасная возможность использовать функцию по умолчанию. MyClass::~MyClass() = default; в файле реализации с меньшей вероятностью будет случайно удален позже кем-то, кто предположит, что тело дестуктора было стерто, а не намеренно оставлено пустым. - person Dennis Zickefoose; 16.05.2011
comment
@Dennis Zickefoose: К сожалению, OP использует VC ++, а VC ++ еще не поддерживает членов классов defaulted и deleted. - person ildjarn; 16.05.2011
comment
+1 за то, как переместить дверь в файл .cpp. Также кажется, что MyClass::~MyClass() = default не перемещает его в файл реализации на Clang. (пока что?) - person eonil; 06.12.2013
comment
Вам также необходимо переместить реализацию конструктора в файл CPP, по крайней мере, в VS 2017. См., Например, этот ответ: stackoverflow.com / a / 27624369/5124002 - person jciloa; 09.05.2019

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

person Puppy    schedule 22.05.2011

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

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

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

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

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

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

person Shital Shah    schedule 10.02.2017
comment
Спасибо! Это было очень сжатое объяснение невероятно непонятной причуды C ++. Избавил меня от многих хлопот. - person JPNotADragon; 01.08.2019

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

Заголовок: A.h

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 снова уничтожается.)

person Joachim    schedule 12.01.2018

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

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

person BЈовић    schedule 22.05.2011

Я искал способ использовать идиому PIMPL с std::unique_ptr. Это руководство - отличный ресурс.

Вкратце, вот что вы можете сделать, чтобы это работало:

my_class.h

#include <memory>

class Thing;

class MyClass
{
    ~MyClass(); // <--- Added
    std::unique_ptr< Thing > my_thing;
};

my_class.cpp

MyClass::~MyClass() = default; // Or a custom implementation
person Paul    schedule 03.10.2020

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

person deltanine    schedule 07.06.2019

Что касается меня,

QList<QSharedPointer<ControllerBase>> controllers;

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

#include <QSharedPointer>
person Sanbrother    schedule 22.08.2018
comment
Ответ не имеет отношения к вопросу и не имеет отношения к нему. - person Mikus; 28.01.2019