std :: unique_ptr з неповним типом не збирається


203

Я використовую ідіому pimpl з std::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

Однак я отримую помилку компіляції щодо використання неповного типу в рядку 304 у <memory>:

Недійсне застосування " sizeof" до неповного типу " uixx::window::window_impl"

Наскільки я знаю, він std::unique_ptrповинен мати можливість використовувати неповний тип. Це помилка в libc ++ чи я роблю тут щось не так?


Посилання посилання на вимоги повноти: stackoverflow.com/a/6089065/576911
— Howard Hinnant

1
Помпа часто будується і з тих пір не змінюється. Зазвичай я використовую std :: shared_ptr <const window_impl>
— mfnx

Супутнє: Я дуже хотів би знати, чому це працює в MSVC, і як не допустити його роботи (щоб я не порушував збірки колег з GCC).
— Лен

Відповіді:


260

Ось кілька прикладів std::unique_ptrіз неповними типами. Проблема полягає в руйнуванні.

Якщо ви використовуєте pimpl з unique_ptr, вам потрібно оголосити деструктор:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

оскільки в іншому випадку компілятор генерує типовий, і для цього йому потрібно повне оголошення foo::impl.

Якщо у вас є конструктори шаблонів, то вас накрутили, навіть якщо ви не конструюєте impl_член:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

У області простору імен використання unique_ptrтакож не буде працювати:

class impl;
std::unique_ptr<impl> impl_;

оскільки компілятор повинен знати тут, як знищити цей статичний об’єкт тривалості. Обхід:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;

3
Я вважаю, що ваше перше рішення (додавання foo destructor) дозволяє самому оголошенню класу збирати, але оголошення об'єкта цього типу де завгодно призводить до початкової помилки ("недійсне застосування 'sizeof' ...").
— Джефф Трулл

38
відмінна відповідь, просто зазначити; ми все ще можемо використовувати конструктор / деструктор за замовчуванням, розмістивши, наприклад, foo::~foo() = default;у файлі src
— збираємося

2
Одним із способів жити з конструкторами шаблонів було б декларувати, але не визначати конструктор в тілі класу, десь визначити його повне визначення імпульсів і явно створювати всі необхідні інстанції там.
— енобайрам

2
Чи можете ви пояснити, як це діятиме в одних випадках, а не в інших? Я використав ідіому pimpl з унікальним_ptr та класом без деструктора, а в іншому проекті мій код не вдасться компілювати із згаданою помилкою ОП.
— Цікаво

1
Здається, якщо для файлу заголовка класу зі стилем c ++ 11 за замовчуванням встановлено значення {default_ptr} {nullptr}, повне оголошення також потрібно з вищевказаної причини.
— feirainy

53

Як зазначав Олександр К. , проблема зводиться до того window, що деструктор неявно визначається в місцях, де тип window_implще є неповним. Окрім його рішень, ще одне вирішення, яке я використав, - оголосити функціонера Deleter у заголовку:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

Зауважте, що використання спеціальної функції «Видалення» виключає використання std::make_unique(доступне на C ++ 14), про що вже говорилося тут .


6
Що стосується мене, це правильне рішення. Це не унікально для використання pimpl-idiom, це загальна проблема із використанням std :: unique_ptr з неповними класами. Делетер за замовчуванням, який використовується std :: unique_ptr <X>, намагається зробити "видалити X", чого він не може зробити, якщо X - пряма декларація. Вказавши функцію видалення, ви можете помістити цю функцію у вихідний файл, де клас X повністю визначений. Інші вихідні файли можуть використовувати std :: unique_ptr <X, DeleterFunc>, хоча X - це лише пряма заява, якщо вони пов'язані з вихідним файлом, що містить DeleterFunc.
— притулок

1
Це хороший спосіб вирішення, коли ви повинні мати вбудоване визначення функції, яке створює екземпляр типу "Foo" (наприклад, статичний метод "getInstance", на який посилаються конструктор і деструктор), і ви не хочете переміщувати їх у файл реалізації як пропонує @ adspx5.
— GameSalutes

20

використовувати спеціальний делетер

Проблема полягає в тому, що він unique_ptr<T>повинен викликати деструктора T::~T()у власному деструкторі, його операторі призначення присвоєння руху та unique_ptr::reset()функції члена (лише). Однак їх потрібно викликати (неявно або явно) у кількох ситуаціях PIMPL (вже у деструкторі зовнішнього класу та операторі призначення присвоєння).

Як вже зазначалося в іншу відповідь, один з способів уникнути цього, щоб перемістити всі операції, що вимагають unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)іunique_ptr::reset() в вихідний файл , в якому фактично визначено клас Pimpl помічник.

Однак це досить незручно і певною мірою заперечує саму точку pimpl idoim. Набагато більш чисте рішення, яке дозволяє уникнути всього, що полягає у використанні користувальницького делетера, і лише перемістити його визначення у вихідний файл, де живе клас помічника прищів. Ось простий приклад:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

Замість окремого класу делетерів ви також можете використовувати вільну функцію або staticчлен fooу поєднанні з лямбда:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};

15

Можливо, у вас є деякі функціональні органи в .h-файлі в класі, який використовує неповний тип.

Переконайтеся, що у вашому .h для вікна класу у вас є лише декларація функції. Усі функціональні органи для вікна повинні бути у файлі .cpp. І для window_impl також ...

До речі, ви повинні явно додати декларацію деструктора для класу Windows у свій .h файл.

Але ви НЕ МОЖЕТЕ помістити порожній корпус dtor у файл заголовка:

class window {
    virtual ~window() {};
  }

Повинно бути лише декларацією:

  class window {
    virtual ~window();
  }

Це було і моє рішення. Шлях більш лаконічний. Просто замовте свій конструктор / деструктор у заголовку та визначено у файлі cpp.
— Кріс Морнес

2

Щоб додати відповіді інших про користувацький делетер, у нашу внутрішню "бібліотеку утиліт" я додав помічник заголовка для реалізації цієї загальної моделі (std::unique_ptr неповного типу, відомого лише деякому з ТУ, наприклад, щоб уникнути тривалого часу компіляції або надати просто непрозора ручка для клієнтів).

Він надає загальні риштування цього шаблону: користувацький клас делетерів, який викликає функцію делетора, визначений зовні, тип псевдоніму для unique_ptrцього класу видалення та макрос для оголошення функції делетора в TU, який має повне визначення тип. Я думаю, що це має певну загальну корисність, тому ось це:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif

1

Можливо, це не найкраще рішення, але іноді ви можете використовувати натомість shared_ptr . Якщо, звичайно, це надмірна кількість, але ... що стосується unique_ptr, я, можливо, зачекаю ще 10 років, поки виробники стандартів C ++ вирішать використовувати лямбда як делетер.

Ще одна сторона. Згідно з вашим кодом, може статися, що на етапі знищення window_impl буде неповним. Це може бути причиною невизначеної поведінки. Дивіться це: Чому, дійсно, видалення незавершеного типу є невизначеною поведінкою?

Отже, якщо можливо, я б визначив дуже базовий об'єкт для всіх ваших об'єктів, з віртуальним деструктором. І ти майже хороший. Вам просто слід пам’ятати, що система викличе віртуальний деструктор для вашого вказівника, тому ви повинні визначити його для кожного предка. Ви також повинні визначити базовий клас у розділі спадкування як віртуальний (детальніше див. Це ).

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