Чи потрібно std :: unique_ptr <T>, щоб знати повне визначення T?


248

Я маю код у заголовку, який виглядає приблизно так:

#include <memory>

class Thing;

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

Якщо я включаю цей заголовок у cpp, який не включає Thingвизначення типу, то це не компілюється під VS2010-SP1:

1> C: \ програмні файли (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, чи не так?


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 це, якби міг. Я впевнений, що буду посилатися на це в своєму наступному проекті, в якому я намагаюся в повній мірі використовувати розумні покажчики.
matthias

4
якщо можна пояснити, що означає таблиця, я здогадуюсь, що це допоможе більшості людей
Ghita

8
Ще одна примітка: Конструктор класу посилається на деструкторів своїх членів (для випадку, коли викинуто виняток, потрібно викликати ці деструктори). Отже, хоча деструктору унікального_ptr потрібен повний тип, недостатньо мати деструктор, визначений користувачем, у класі - він також потребує конструктора.
Йоханнес Шауб - ліб

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

@Dennis Zickefoose: На жаль, в ОП використовується VC ++, а VC ++ ще не підтримує членів класу defaulted та deleted.
ildjarn

6
+1 про те, як перемістити двері у .cpp-файл. Крім того, схоже, MyClass::~MyClass() = defaultце не переміщує його у файл реалізації на Clang. (ще?)
Еоніл

Крім того, необхідно перемістити реалізацію конструктора в файл CPP, принаймні , на VS 2017. Див, наприклад , ця відповідь: stackoverflow.com/a/27624369/5124002
jciloa

15

Це не залежить від реалізації. Причина його роботи полягає в тому, що shared_ptrвизначає правильний деструктор для виклику під час виконання - він не є частиною підпису типу. Однак unique_ptrдеструктор 's є частиною його типу, і це необхідно знати під час компіляції.


8

Схоже, що поточні відповіді не точно прибивають, чому конструктор (або деструктор) за замовчуванням є проблемою, але порожні, оголошені в cpp, не є.

Ось що відбувається:

Якщо зовнішній клас (тобто MyClass) не має конструктора чи деструктора, тоді компілятор генерує типові. Проблема в тому, що компілятор по суті вставляє порожній конструктор / деструктор за замовчуванням у файл .hpp. Це означає, що код за замовчуванням кондуктора / деструктора збирається разом із двійковим файлом виконавчого файлу хоста, а не бінарними файлами вашої бібліотеки. Однак це визначення насправді не може побудувати часткові класи. Отже, коли лінкер переходить у бінарний файл вашої бібліотеки і намагається отримати конструктор / деструктор, він не знаходить жодного, і ви отримуєте помилку. Якщо код конструктора / деструктора був у вашому .cpp, то у вашій бінарній бібліотеці є доступна для посилання.

Це не має нічого спільного з використанням унікального_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. І це має сенс при розгляді що у випадку винятку в конструкторі унікальний_ptr знову знищується.)


1

Повне визначення речі потрібно в точці опису шаблону. Це точна причина, через яку ідіома складається з pimpl.

Якщо це не вдалося, люди не будуть задавати питання , як це .



-7

Як на мене,

QList<QSharedPointer<ControllerBase>> controllers;

Просто включіть заголовок ...

#include <QSharedPointer>

Відповідь не стосується і не стосується питання.
Mikus
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.